Compare commits
25 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e31c7908cd | |||
| d916afc2ec | |||
| e31a78add1 | |||
| 687de09eb4 | |||
| 823c45f054 | |||
| 5b49868de7 | |||
| d1e35aa832 | |||
| 1e69ae6898 | |||
| a9e4e3ea2d | |||
| 01766b79e5 | |||
| abb8038c12 | |||
| fcc5734b8d | |||
| 49c1750c19 | |||
| d1634c113c | |||
| 36a6169f4e | |||
| 60bf9b8c50 | |||
| dfa009e7df | |||
| db950f3f7a | |||
| fe9cc15818 | |||
| 34fa512778 | |||
| e63591492a | |||
| 7a0b979b01 | |||
| eaab442794 | |||
| 78d6dcc28c | |||
| 53c8200c5a |
@@ -13,8 +13,7 @@ jobs:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: oven-sh/setup-bun@v2
|
||||
- run: bun install
|
||||
- run: bun tsc --noEmit
|
||||
- run: bunx eslint . --config eslint.config.js --max-warnings=999999
|
||||
- run: bun run lint
|
||||
|
||||
test:
|
||||
name: Test
|
||||
|
||||
Vendored
+1
@@ -21,6 +21,7 @@
|
||||
"ccstatusline",
|
||||
"Powerline",
|
||||
"statusline",
|
||||
"sublabel",
|
||||
"Worktree",
|
||||
"worktrees"
|
||||
]
|
||||
|
||||
@@ -33,7 +33,10 @@ bun test
|
||||
bun test --watch
|
||||
|
||||
# Lint and type check
|
||||
bun run lint # Runs TypeScript type checking and ESLint with auto-fix
|
||||
bun run lint # Runs TypeScript type checking and ESLint without modifying files
|
||||
|
||||
# Apply ESLint auto-fixes intentionally
|
||||
bun run lint:fix
|
||||
```
|
||||
|
||||
## Architecture
|
||||
@@ -136,7 +139,7 @@ Default to using Bun instead of Node.js:
|
||||
2. `postbuild`: Runs scripts/replace-version.ts to replace `__PACKAGE_VERSION__` placeholder with actual version from package.json
|
||||
- **ESLint configuration**: Uses flat config format (eslint.config.js) with TypeScript and React plugins
|
||||
- **Dependencies**: All runtime dependencies are bundled using `--packages=external` for npm package
|
||||
- **Type checking and linting**: Only run via `bun run lint` command, never using `npx eslint` or `eslint` directly. Never run `tsx`, `bun tsc` or any other variation
|
||||
- **Type checking and linting**: Run checks via `bun run lint` and use `bun run lint:fix` only when you intentionally want ESLint auto-fixes. Never use `npx eslint`, `eslint`, `tsx`, `bun tsc`, or any other variation directly
|
||||
- **Lint rules**: Never disable a lint rule via a comment, no matter how benign the lint warning or error may seem
|
||||
- **Testing**: Uses Vitest (via Bun) with 6 test files and ~40 test cases covering:
|
||||
- Model context detection and token calculation (src/utils/__tests__/model-context.test.ts)
|
||||
|
||||
@@ -46,12 +46,18 @@
|
||||
|
||||
## 🆕 Recent Updates
|
||||
|
||||
### v2.2.0 - New Token Speed widgets with optional windows
|
||||
### v2.2.0 - v2.2.6 - Speed, widgets, links, and reliability updates
|
||||
|
||||
- **🚀 New Token Speed widgets** - Added three widgets: **Input Speed**, **Output Speed**, and **Total Speed**.
|
||||
- Each speed widget supports a configurable window of `0-120` seconds in the widget editor (`w` key).
|
||||
- `0` disables window mode and uses a full-session average speed.
|
||||
- `1-120` calculates recent speed over the selected rolling window.
|
||||
- **🧩 New Skills widget controls (v2.2.1)** - Added configurable Skills modes (last/count/list), optional hide-when-empty behavior, and list-size limiting with most-recent-first ordering.
|
||||
- **🌐 Usage API proxy support (v2.2.2)** - Usage widgets honor the uppercase `HTTPS_PROXY` environment variable for their direct API call to Anthropic.
|
||||
- **🧠 New Thinking Effort widget (v2.2.4)** - Added a widget that shows the current Claude Code thinking effort level.
|
||||
- **🍎 Better macOS usage lookup reliability (v2.2.5)** - Improved reliability when loading usage API tokens on macOS.
|
||||
- **⌨️ New Vim Mode widget (v2.2.5)** - Added a widget that shows the current vim mode, with ASCII and optional Nerd Font icon display.
|
||||
- **🔗 Git widget link modes (v2.2.6)** - `Git Branch` can render clickable GitHub branch links, and `Git Root Dir` can render clickable IDE links for VS Code and Cursor.
|
||||
- **🤝 Better subagent-aware speed reporting** - Token speed calculations continue to include referenced subagent activity so displayed speeds better reflect actual concurrent work.
|
||||
|
||||
### v2.1.0 - v2.1.10 - Usage widgets, links, new git insertions / deletions widgets, and reliability fixes
|
||||
@@ -202,6 +208,8 @@ The interactive configuration tool provides a terminal UI where you can:
|
||||
> $env:CLAUDE_CONFIG_DIR="C:\custom\path\.claude"
|
||||
> ```
|
||||
|
||||
> 🌐 **Usage API proxy:** Usage widgets honor the uppercase `HTTPS_PROXY` environment variable for their direct API call to Anthropic.
|
||||
|
||||
### Claude Code settings.json format
|
||||
|
||||
When you install from the TUI, ccstatusline writes a `statusLine` command object to your Claude Code settings:
|
||||
@@ -424,39 +432,44 @@ bun run example
|
||||
|
||||
### 📊 Available Widgets
|
||||
|
||||
- **Model Name** - Shows the current Claude model (e.g., "Claude 3.5 Sonnet")
|
||||
- **Git Branch** - Displays current git branch name
|
||||
- **Git Changes** - Shows uncommitted insertions/deletions (e.g., "+42,-10")
|
||||
- **Git Insertions** - Shows uncommitted insertions only (e.g., "+42")
|
||||
- **Git Deletions** - Shows uncommitted deletions only (e.g., "-10")
|
||||
- **Model** - Displays the Claude model name (e.g., "Claude 3.5 Sonnet")
|
||||
- **Output Style** - Shows the current Claude Code output style
|
||||
- **Git Branch** - Shows the current git branch name
|
||||
- **Git Changes** - Shows git changes count (`+insertions`, `-deletions`)
|
||||
- **Git Insertions** - Shows git insertions count
|
||||
- **Git Deletions** - Shows git deletions count
|
||||
- **Git Root Dir** - Shows the git repository root directory name
|
||||
- **Git Worktree** - Shows the name of the current git worktree
|
||||
- **Session Clock** - Shows elapsed time since session start (e.g., "2hr 15m")
|
||||
- **Session Usage** - Shows current 5-hour/session API usage percentage
|
||||
- **Weekly Usage** - Shows rolling 7-day API usage percentage
|
||||
- **Session Cost** - Shows total session cost in USD (e.g., "$1.23")
|
||||
- **Session Name** - Shows the session name set via `/rename` command in Claude Code
|
||||
- **Git Worktree** - Shows the current git worktree name
|
||||
- **Current Working Dir** - Shows current working directory with segment limit, fish-style abbreviation, and optional `~` home abbreviation
|
||||
- **Tokens Input** - Shows input token count for the current session
|
||||
- **Tokens Output** - Shows output token count for the current session
|
||||
- **Tokens Cached** - Shows cached token count for the current session
|
||||
- **Tokens Total** - Shows total token count (`input + output + cache`) for the current session
|
||||
- **Input Speed** - Shows session-average input token speed (`tokens/sec`) with optional per-widget window (`0-120` seconds; `0` = full-session average)
|
||||
- **Output Speed** - Shows session-average output token speed (`tokens/sec`) with optional per-widget window (`0-120` seconds; `0` = full-session average)
|
||||
- **Total Speed** - Shows session-average total token speed (`tokens/sec`) with optional per-widget window (`0-120` seconds; `0` = full-session average)
|
||||
- **Context Length** - Shows the current context window size in tokens
|
||||
- **Context %** - Shows percentage of context window used or remaining
|
||||
- **Context % (usable)** - Shows percentage of usable context used or remaining (80% of max before auto-compact)
|
||||
- **Session Clock** - Shows elapsed time since current session started
|
||||
- **Session Cost** - Shows the total session cost in USD
|
||||
- **Block Timer** - Shows current 5-hour block elapsed time or progress
|
||||
- **Terminal Width** - Shows current terminal width in columns
|
||||
- **Version** - Shows Claude Code CLI version number
|
||||
- **Custom Text** - Displays user-defined custom text
|
||||
- **Custom Command** - Executes a custom shell command and displays output (refreshes whenever Claude Code updates the status line)
|
||||
- **Link** - Displays a clickable terminal hyperlink using OSC 8
|
||||
- **Claude Session ID** - Shows the current Claude Code session ID from status JSON
|
||||
- **Block Timer** - Shows time elapsed in current 5-hour block or progress bar
|
||||
- **Block Reset Timer** - Shows time remaining until the current 5-hour block resets
|
||||
- **Weekly Reset Timer** - Shows time remaining until the weekly usage window resets
|
||||
- **Current Working Directory** - Shows current working directory with segment limit, fish-style abbreviation, and optional `~` home abbreviation
|
||||
- **Version** - Shows Claude Code version
|
||||
- **Output Style** - Shows the currently set output style in Claude Code
|
||||
- **Tokens Input** - Shows input tokens used
|
||||
- **Tokens Output** - Shows output tokens used
|
||||
- **Tokens Cached** - Shows cached tokens used
|
||||
- **Tokens Total** - Shows total tokens used
|
||||
- **Context Length** - Shows current context length in tokens
|
||||
- **Context Percentage** - Shows percentage of context limit used (dynamic: 1M for model IDs with long-context labels like `[1m]` or `1M context`, 200k otherwise)
|
||||
- **Context Percentage (usable)** - Shows percentage of usable context (dynamic: 800k for model IDs with long-context labels like `[1m]` or `1M context`, 160k otherwise, accounting for auto-compact at 80%)
|
||||
- **Session Name** - Shows the session name set via `/rename` in Claude Code
|
||||
- **Memory Usage** - Shows system memory usage (used/total)
|
||||
- **Session Usage** - Shows daily/session API usage percentage
|
||||
- **Weekly Usage** - Shows weekly API usage percentage
|
||||
- **Block Reset Timer** - Shows time remaining until current 5-hour block reset window
|
||||
- **Weekly Reset Timer** - Shows time remaining until weekly usage reset
|
||||
- **Context Bar** - Shows context usage as a progress bar with short/full display modes
|
||||
- **Terminal Width** - Shows detected terminal width (for debugging)
|
||||
- **Memory Usage** - Shows system memory usage (used/total, e.g., "Mem: 12.4G/16.0G")
|
||||
- **Custom Text** - Add your own custom text to the status line
|
||||
- **Custom Command** - Execute shell commands and display their output (refreshes whenever the statusline is updated by Claude Code)
|
||||
- **Link** - Add clickable terminal hyperlinks (OSC 8) with configurable URL and display text
|
||||
- **Separator** - Visual divider between widgets (customizable: |, -, comma, space; available when Powerline mode is off and no default separator is configured)
|
||||
- **Skills** - Shows skill activity as last used, total count, or unique list (with optional list limit and hide-when-empty toggle)
|
||||
- **Vim Mode** - Displays current vim editor mode
|
||||
- **Separator** - Visual divider between widgets (available when Powerline mode is off and no default separator is configured)
|
||||
- **Flex Separator** - Expands to fill available space (available when Powerline mode is off)
|
||||
|
||||
---
|
||||
@@ -668,9 +681,12 @@ bun run example
|
||||
# Run tests
|
||||
bun test
|
||||
|
||||
# Run typecheck + eslint autofix
|
||||
# Run typecheck + eslint checks without modifying files
|
||||
bun run lint
|
||||
|
||||
# Apply ESLint auto-fixes intentionally
|
||||
bun run lint:fix
|
||||
|
||||
# Build for distribution
|
||||
bun run build
|
||||
|
||||
|
||||
@@ -1,24 +1,26 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "ccstatusline",
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.33.0",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@stylistic/eslint-plugin": "^5.2.3",
|
||||
"@types/bun": "latest",
|
||||
"@types/pluralize": "^0.0.33",
|
||||
"@types/react": "^19.1.10",
|
||||
"chalk": "^5.5.0",
|
||||
"eslint": "^9.33.0",
|
||||
"eslint": "^10.0.0",
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-import-newlines": "^1.4.0",
|
||||
"eslint-plugin-import-newlines": "^2.0.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"globals": "^14.0.0",
|
||||
"https-proxy-agent": "^7.0.0",
|
||||
"ink": "6.2.0",
|
||||
"ink-gradient": "^3.0.0",
|
||||
"ink-gradient": "^4.0.0",
|
||||
"ink-select-input": "^6.2.0",
|
||||
"pluralize": "^8.0.0",
|
||||
"react": "^19.1.1",
|
||||
@@ -74,87 +76,33 @@
|
||||
|
||||
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
|
||||
|
||||
"@emnapi/core": ["@emnapi/core@1.4.5", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.4", "tslib": "^2.4.0" } }, "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q=="],
|
||||
"@emnapi/core": ["@emnapi/core@1.9.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.4.5", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg=="],
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.9.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw=="],
|
||||
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.4", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.9", "", { "os": "aix", "cpu": "ppc64" }, "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.9", "", { "os": "android", "cpu": "arm" }, "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.9", "", { "os": "android", "cpu": "arm64" }, "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.9", "", { "os": "android", "cpu": "x64" }, "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.9", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.9", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.9", "", { "os": "linux", "cpu": "arm" }, "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.9", "", { "os": "linux", "cpu": "ia32" }, "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.9", "", { "os": "linux", "cpu": "none" }, "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.9", "", { "os": "linux", "cpu": "none" }, "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.9", "", { "os": "linux", "cpu": "ppc64" }, "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.9", "", { "os": "linux", "cpu": "none" }, "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.9", "", { "os": "linux", "cpu": "s390x" }, "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.9", "", { "os": "linux", "cpu": "x64" }, "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.9", "", { "os": "none", "cpu": "arm64" }, "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.9", "", { "os": "none", "cpu": "x64" }, "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.9", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.9", "", { "os": "openbsd", "cpu": "x64" }, "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.9", "", { "os": "none", "cpu": "arm64" }, "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.9", "", { "os": "sunos", "cpu": "x64" }, "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.9", "", { "os": "win32", "cpu": "ia32" }, "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.9", "", { "os": "win32", "cpu": "x64" }, "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ=="],
|
||||
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="],
|
||||
|
||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="],
|
||||
|
||||
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="],
|
||||
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
||||
|
||||
"@eslint/config-array": ["@eslint/config-array@0.21.1", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA=="],
|
||||
"@eslint/config-array": ["@eslint/config-array@0.23.3", "", { "dependencies": { "@eslint/object-schema": "^3.0.3", "debug": "^4.3.1", "minimatch": "^10.2.4" } }, "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw=="],
|
||||
|
||||
"@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw=="],
|
||||
"@eslint/config-helpers": ["@eslint/config-helpers@0.5.3", "", { "dependencies": { "@eslint/core": "^1.1.1" } }, "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw=="],
|
||||
|
||||
"@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ=="],
|
||||
"@eslint/core": ["@eslint/core@1.1.1", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ=="],
|
||||
|
||||
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.1", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ=="],
|
||||
"@eslint/js": ["@eslint/js@10.0.1", "", { "peerDependencies": { "eslint": "^10.0.0" }, "optionalPeers": ["eslint"] }, "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA=="],
|
||||
|
||||
"@eslint/js": ["@eslint/js@9.39.3", "", {}, "sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw=="],
|
||||
"@eslint/object-schema": ["@eslint/object-schema@3.0.3", "", {}, "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ=="],
|
||||
|
||||
"@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA=="],
|
||||
|
||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="],
|
||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.6.1", "", { "dependencies": { "@eslint/core": "^1.1.1", "levn": "^0.4.1" } }, "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ=="],
|
||||
|
||||
"@gerrit0/mini-shiki": ["@gerrit0/mini-shiki@3.23.0", "", { "dependencies": { "@shikijs/engine-oniguruma": "^3.23.0", "@shikijs/langs": "^3.23.0", "@shikijs/themes": "^3.23.0", "@shikijs/types": "^3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-bEMORlG0cqdjVyCEuU0cDQbORWX+kYCeo0kV1lbxF5bt4r7SID2l9bqsxJEM0zndaxpOUT7riCyIVEuqq/Ynxg=="],
|
||||
|
||||
"@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
|
||||
|
||||
"@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
|
||||
"@humanfs/node": ["@humanfs/node@0.16.7", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ=="],
|
||||
|
||||
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
|
||||
|
||||
@@ -172,45 +120,41 @@
|
||||
|
||||
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.49.0", "", { "os": "android", "cpu": "arm" }, "sha512-rlKIeL854Ed0e09QGYFlmDNbka6I3EQFw7iZuugQjMb11KMpJCLPFL4ZPbMfaEhLADEL1yx0oujGkBQ7+qW3eA=="],
|
||||
"@oxc-project/runtime": ["@oxc-project/runtime@0.115.0", "", {}, "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.49.0", "", { "os": "android", "cpu": "arm64" }, "sha512-cqPpZdKUSQYRtLLr6R4X3sD4jCBO1zUmeo3qrWBCqYIeH8Q3KRL4F3V7XJ2Rm8/RJOQBZuqzQGWPjjvFUcYa/w=="],
|
||||
"@oxc-project/types": ["@oxc-project/types@0.115.0", "", {}, "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.49.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-99kMMSMQT7got6iYX3yyIiJfFndpojBmkHfTc1rIje8VbjhmqBXE+nb7ZZP3A5skLyujvT0eIUCUsxAe6NjWbw=="],
|
||||
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.9", "", { "os": "android", "cpu": "arm64" }, "sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.49.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-y8cXoD3wdWUDpjOLMKLx6l+NFz3NlkWKcBCBfttUn+VGSfgsQ5o/yDUGtzE9HvsodkP0+16N0P4Ty1VuhtRUGg=="],
|
||||
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.49.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-3mY5Pr7qv4GS4ZvWoSP8zha8YoiqrU+e0ViPvB549jvliBbdNLrg2ywPGkgLC3cmvN8ya3za+Q2xVyT6z+vZqA=="],
|
||||
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.49.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-C9KzzOAQU5gU4kG8DTk+tjdKjpWhVWd5uVkinCwwFub2m7cDYLOdtXoMrExfeBmeRy9kBQMkiyJ+HULyF1yj9w=="],
|
||||
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.9", "", { "os": "freebsd", "cpu": "x64" }, "sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.49.0", "", { "os": "linux", "cpu": "arm" }, "sha512-OVSQgEZDVLnTbMq5NBs6xkmz3AADByCWI4RdKSFNlDsYXdFtlxS59J+w+LippJe8KcmeSSM3ba+GlsM9+WwC1w=="],
|
||||
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.9", "", { "os": "linux", "cpu": "arm" }, "sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.49.0", "", { "os": "linux", "cpu": "arm" }, "sha512-ZnfSFA7fDUHNa4P3VwAcfaBLakCbYaxCk0jUnS3dTou9P95kwoOLAMlT3WmEJDBCSrOEFFV0Y1HXiwfLYJuLlA=="],
|
||||
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.49.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z81u+gfrobVK2iV7GqZCBfEB1y6+I61AH466lNK+xy1jfqFLiQ9Qv716WUM5fxFrYxwC7ziVdZRU9qvGHkYIJg=="],
|
||||
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.49.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-zoAwS0KCXSnTp9NH/h9aamBAIve0DXeYpll85shf9NJ0URjSTzzS+Z9evmolN+ICfD3v8skKUPyk2PO0uGdFqg=="],
|
||||
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.9", "", { "os": "linux", "cpu": "ppc64" }, "sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w=="],
|
||||
|
||||
"@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.49.0", "", { "os": "linux", "cpu": "none" }, "sha512-2QyUyQQ1ZtwZGiq0nvODL+vLJBtciItC3/5cYN8ncDQcv5avrt2MbKt1XU/vFAJlLta5KujqyHdYtdag4YEjYQ=="],
|
||||
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.9", "", { "os": "linux", "cpu": "s390x" }, "sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.49.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-k9aEmOWt+mrMuD3skjVJSSxHckJp+SiFzFG+v8JLXbc/xi9hv2icSkR3U7uQzqy+/QbbYY7iNB9eDTwrELo14g=="],
|
||||
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.9", "", { "os": "linux", "cpu": "x64" }, "sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.49.0", "", { "os": "linux", "cpu": "none" }, "sha512-rDKRFFIWJ/zJn6uk2IdYLc09Z7zkE5IFIOWqpuU0o6ZpHcdniAyWkwSUWE/Z25N/wNDmFHHMzin84qW7Wzkjsw=="],
|
||||
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.9", "", { "os": "linux", "cpu": "x64" }, "sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.49.0", "", { "os": "linux", "cpu": "none" }, "sha512-FkkhIY/hYFVnOzz1WeV3S9Bd1h0hda/gRqvZCMpHWDHdiIHn6pqsY3b5eSbvGccWHMQ1uUzgZTKS4oGpykf8Tw=="],
|
||||
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.9", "", { "os": "none", "cpu": "arm64" }, "sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.49.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-gRf5c+A7QiOG3UwLyOOtyJMD31JJhMjBvpfhAitPAoqZFcOeK3Kc1Veg1z/trmt+2P6F/biT02fU19GGTS529A=="],
|
||||
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.9", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.1.1" }, "cpu": "none" }, "sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.49.0", "", { "os": "linux", "cpu": "x64" }, "sha512-BR7+blScdLW1h/2hB/2oXM+dhTmpW3rQt1DeSiCP9mc2NMMkqVgjIN3DDsNpKmezffGC9R8XKVOLmBkRUcK/sA=="],
|
||||
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.49.0", "", { "os": "linux", "cpu": "x64" }, "sha512-hDMOAe+6nX3V5ei1I7Au3wcr9h3ktKzDvF2ne5ovX8RZiAHEtX1A5SNNk4zt1Qt77CmnbqT+upb/umzoPMWiPg=="],
|
||||
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.9", "", { "os": "win32", "cpu": "x64" }, "sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.49.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-wkNRzfiIGaElC9kXUT+HLx17z7D0jl+9tGYRKwd8r7cUqTL7GYAvgUY++U2hK6Ar7z5Z6IRRoWC8kQxpmM7TDA=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.49.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-gq5aW/SyNpjp71AAzroH37DtINDcX1Qw2iv9Chyz49ZgdOP3NV8QCyKZUrGsYX9Yyggj5soFiRCgsL3HwD8TdA=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.49.0", "", { "os": "win32", "cpu": "x64" }, "sha512-gEtqFbzmZLFk2xKh7g0Rlo8xzho8KrEFEkzvHbfUGkrgXOpZ4XagQ6n+wIZFNh1nTb8UD16J4nFSFKXYgnbdBg=="],
|
||||
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.9", "", {}, "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw=="],
|
||||
|
||||
"@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],
|
||||
|
||||
@@ -226,16 +170,18 @@
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@stylistic/eslint-plugin": ["@stylistic/eslint-plugin@5.9.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/types": "^8.56.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "estraverse": "^5.3.0", "picomatch": "^4.0.3" }, "peerDependencies": { "eslint": "^9.0.0 || ^10.0.0" } }, "sha512-FqqSkvDMYJReydrMhlugc71M76yLLQWNfmGq+SIlLa7N3kHp8Qq8i2PyWrVNAfjOyOIY+xv9XaaYwvVW7vroMA=="],
|
||||
"@stylistic/eslint-plugin": ["@stylistic/eslint-plugin@5.10.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/types": "^8.56.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "estraverse": "^5.3.0", "picomatch": "^4.0.3" }, "peerDependencies": { "eslint": "^9.0.0 || ^10.0.0" } }, "sha512-nPK52ZHvot8Ju/0A4ucSX1dcPV2/1clx0kLcH5wDmrE4naKso7TUC/voUyU1O9OTKTrR6MYip6LP0ogEMQ9jPQ=="],
|
||||
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="],
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
|
||||
|
||||
"@types/bun": ["@types/bun@1.2.20", "", { "dependencies": { "bun-types": "1.2.20" } }, "sha512-dX3RGzQ8+KgmMw7CsW4xT5ITBSCrSbfHc36SNT31EOUg/LA9JWq0VDdEXDRSe1InVWpd2yLUM1FUF/kEOyTzYA=="],
|
||||
"@types/bun": ["@types/bun@1.3.10", "", { "dependencies": { "bun-types": "1.3.10" } }, "sha512-0+rlrUrOrTSskibryHbvQkDOWRJwJZqZlxrUs1u4oOoTln8+WIXBPmAuCF35SWB2z4Zl3E84Nl/D0P7803nigQ=="],
|
||||
|
||||
"@types/chai": ["@types/chai@5.2.2", "", { "dependencies": { "@types/deep-eql": "*" } }, "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg=="],
|
||||
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
|
||||
|
||||
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
|
||||
|
||||
"@types/esrecurse": ["@types/esrecurse@4.3.1", "", {}, "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/gradient-string": ["@types/gradient-string@1.1.6", "", { "dependencies": { "@types/tinycolor2": "*" } }, "sha512-LkaYxluY4G5wR1M4AKQUal2q61Di1yVVCw42ImFTuaIoQVgmV0WP1xUaLB8zwb47mp82vWTpePI9JmrjEnJ7nQ=="],
|
||||
@@ -246,7 +192,7 @@
|
||||
|
||||
"@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
|
||||
|
||||
"@types/node": ["@types/node@24.2.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-3xyG3pMCq3oYCNg7/ZP+E1ooTaGB4cG8JWRsqqOYQdbWNY4zbaV0Ennrd7stjiJEFZCaybcIgpTjJWHRfBSIDw=="],
|
||||
"@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="],
|
||||
|
||||
"@types/pluralize": ["@types/pluralize@0.0.33", "", {}, "sha512-JOqsl+ZoCpP4e8TDke9W79FDcSgPAR0l6pixx2JHkhnRjvShyYiAYw2LVsnA7K08Y6DeOnaU6ujmENO4os/cYg=="],
|
||||
|
||||
@@ -256,25 +202,25 @@
|
||||
|
||||
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.56.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/type-utils": "8.56.1", "@typescript-eslint/utils": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.56.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A=="],
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.57.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/type-utils": "8.57.0", "@typescript-eslint/utils": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.57.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ=="],
|
||||
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.56.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg=="],
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.57.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g=="],
|
||||
|
||||
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.56.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.56.1", "@typescript-eslint/types": "^8.56.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ=="],
|
||||
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.57.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.57.0", "@typescript-eslint/types": "^8.57.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w=="],
|
||||
|
||||
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1" } }, "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w=="],
|
||||
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0" } }, "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw=="],
|
||||
|
||||
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.56.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ=="],
|
||||
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.57.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA=="],
|
||||
|
||||
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg=="],
|
||||
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0", "@typescript-eslint/utils": "8.57.0", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ=="],
|
||||
|
||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.56.1", "", {}, "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw=="],
|
||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.57.0", "", {}, "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.56.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.56.1", "@typescript-eslint/tsconfig-utils": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg=="],
|
||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.57.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.57.0", "@typescript-eslint/tsconfig-utils": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/visitor-keys": "8.57.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.0.0" } }, "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q=="],
|
||||
|
||||
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.56.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA=="],
|
||||
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.57.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/types": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ=="],
|
||||
|
||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.56.1", "", { "dependencies": { "@typescript-eslint/types": "8.56.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw=="],
|
||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.0", "", { "dependencies": { "@typescript-eslint/types": "8.57.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg=="],
|
||||
|
||||
"@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="],
|
||||
|
||||
@@ -314,25 +260,27 @@
|
||||
|
||||
"@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="],
|
||||
|
||||
"@vitest/expect": ["@vitest/expect@4.0.18", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "chai": "^6.2.1", "tinyrainbow": "^3.0.3" } }, "sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ=="],
|
||||
"@vitest/expect": ["@vitest/expect@4.1.0", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.0", "@vitest/utils": "4.1.0", "chai": "^6.2.2", "tinyrainbow": "^3.0.3" } }, "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA=="],
|
||||
|
||||
"@vitest/mocker": ["@vitest/mocker@4.0.18", "", { "dependencies": { "@vitest/spy": "4.0.18", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ=="],
|
||||
"@vitest/mocker": ["@vitest/mocker@4.1.0", "", { "dependencies": { "@vitest/spy": "4.1.0", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw=="],
|
||||
|
||||
"@vitest/pretty-format": ["@vitest/pretty-format@4.0.18", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw=="],
|
||||
"@vitest/pretty-format": ["@vitest/pretty-format@4.1.0", "", { "dependencies": { "tinyrainbow": "^3.0.3" } }, "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A=="],
|
||||
|
||||
"@vitest/runner": ["@vitest/runner@4.0.18", "", { "dependencies": { "@vitest/utils": "4.0.18", "pathe": "^2.0.3" } }, "sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw=="],
|
||||
"@vitest/runner": ["@vitest/runner@4.1.0", "", { "dependencies": { "@vitest/utils": "4.1.0", "pathe": "^2.0.3" } }, "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ=="],
|
||||
|
||||
"@vitest/snapshot": ["@vitest/snapshot@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA=="],
|
||||
"@vitest/snapshot": ["@vitest/snapshot@4.1.0", "", { "dependencies": { "@vitest/pretty-format": "4.1.0", "@vitest/utils": "4.1.0", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg=="],
|
||||
|
||||
"@vitest/spy": ["@vitest/spy@4.0.18", "", {}, "sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw=="],
|
||||
"@vitest/spy": ["@vitest/spy@4.1.0", "", {}, "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw=="],
|
||||
|
||||
"@vitest/utils": ["@vitest/utils@4.0.18", "", { "dependencies": { "@vitest/pretty-format": "4.0.18", "tinyrainbow": "^3.0.3" } }, "sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA=="],
|
||||
"@vitest/utils": ["@vitest/utils@4.1.0", "", { "dependencies": { "@vitest/pretty-format": "4.1.0", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.0.3" } }, "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw=="],
|
||||
|
||||
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||
|
||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||
|
||||
"ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
||||
"agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="],
|
||||
|
||||
"ajv": ["ajv@6.14.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
|
||||
|
||||
"ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="],
|
||||
|
||||
@@ -358,21 +306,23 @@
|
||||
|
||||
"arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="],
|
||||
|
||||
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
|
||||
|
||||
"async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
|
||||
|
||||
"auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="],
|
||||
|
||||
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.0", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA=="],
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.8", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
"brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
|
||||
|
||||
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.20", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-pxTnQYOrKvdOwyiyd/7sMt9yFOenN004Y6O4lCcCUoKVej48FS5cvTw9geRaEcB9TsDZaJKAxPTVvi8tFsVuXA=="],
|
||||
"bun-types": ["bun-types@1.3.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-tcpfCCl6XWo6nCVnpcVrxQ+9AYN1iqMIzgrSKYMB/fjLtV2eyAVEg7AxQJuCq/26R6HpKWykQXuSOq/21RYcbg=="],
|
||||
|
||||
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],
|
||||
|
||||
@@ -380,9 +330,7 @@
|
||||
|
||||
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
|
||||
|
||||
"callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001776", "", {}, "sha512-sg01JDPzZ9jGshqKSckOQthXnYwOEP50jeVFhaSFbZcOy05TiuuaffDOfcwtCisJ9kNQuLBFibYywv2Bgm9osw=="],
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001779", "", {}, "sha512-U5og2PN7V4DMgF50YPNtnZJGWVLFjjsN3zb6uMT5VGYIewieDj1upwfuVNXf4Kor+89c3iCRJnSzMD5LmTvsfA=="],
|
||||
|
||||
"chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
|
||||
|
||||
@@ -396,10 +344,6 @@
|
||||
|
||||
"code-excerpt": ["code-excerpt@4.0.0", "", { "dependencies": { "convert-to-spaces": "^2.0.1" } }, "sha512-xxodCmBen3iy2i0WtAK8FlFNrRzjUqjRsMfho58xT/wvZU1YTM3fCnRjcy1gJPMepaRlgm/0e6w8SpWHpn3/cA=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
@@ -416,7 +360,7 @@
|
||||
|
||||
"data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="],
|
||||
|
||||
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
|
||||
|
||||
@@ -424,27 +368,29 @@
|
||||
|
||||
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.307", "", {}, "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg=="],
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.313", "", {}, "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA=="],
|
||||
|
||||
"emoji-regex": ["emoji-regex@10.4.0", "", {}, "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw=="],
|
||||
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
|
||||
|
||||
"entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
|
||||
|
||||
"environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="],
|
||||
|
||||
"es-abstract": ["es-abstract@1.24.0", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg=="],
|
||||
"es-abstract": ["es-abstract@1.24.1", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw=="],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
|
||||
|
||||
"es-iterator-helpers": ["es-iterator-helpers@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.0.3", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.6", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.4", "safe-array-concat": "^1.1.3" } }, "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w=="],
|
||||
"es-iterator-helpers": ["es-iterator-helpers@1.3.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.1", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", "math-intrinsics": "^1.1.0", "safe-array-concat": "^1.1.3" } }, "sha512-zWwRvqWiuBPr0muUG/78cW3aHROFCNIQ3zpmYDpwdbnt2m+xlNyRWpHBpa2lJjSBit7BQ+RXA1iwbSmu5yJ/EQ=="],
|
||||
|
||||
"es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="],
|
||||
"es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
|
||||
|
||||
@@ -456,13 +402,11 @@
|
||||
|
||||
"es-toolkit": ["es-toolkit@1.45.1", "", {}, "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw=="],
|
||||
|
||||
"esbuild": ["esbuild@0.25.9", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.9", "@esbuild/android-arm": "0.25.9", "@esbuild/android-arm64": "0.25.9", "@esbuild/android-x64": "0.25.9", "@esbuild/darwin-arm64": "0.25.9", "@esbuild/darwin-x64": "0.25.9", "@esbuild/freebsd-arm64": "0.25.9", "@esbuild/freebsd-x64": "0.25.9", "@esbuild/linux-arm": "0.25.9", "@esbuild/linux-arm64": "0.25.9", "@esbuild/linux-ia32": "0.25.9", "@esbuild/linux-loong64": "0.25.9", "@esbuild/linux-mips64el": "0.25.9", "@esbuild/linux-ppc64": "0.25.9", "@esbuild/linux-riscv64": "0.25.9", "@esbuild/linux-s390x": "0.25.9", "@esbuild/linux-x64": "0.25.9", "@esbuild/netbsd-arm64": "0.25.9", "@esbuild/netbsd-x64": "0.25.9", "@esbuild/openbsd-arm64": "0.25.9", "@esbuild/openbsd-x64": "0.25.9", "@esbuild/openharmony-arm64": "0.25.9", "@esbuild/sunos-x64": "0.25.9", "@esbuild/win32-arm64": "0.25.9", "@esbuild/win32-ia32": "0.25.9", "@esbuild/win32-x64": "0.25.9" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g=="],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||
|
||||
"eslint": ["eslint@9.39.3", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.1", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", "@eslint/js": "9.39.3", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg=="],
|
||||
"eslint": ["eslint@10.0.3", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", "@eslint/config-array": "^0.23.3", "@eslint/config-helpers": "^0.5.2", "@eslint/core": "^1.1.1", "@eslint/plugin-kit": "^0.6.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", "espree": "^11.1.1", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ=="],
|
||||
|
||||
"eslint-import-context": ["eslint-import-context@0.1.9", "", { "dependencies": { "get-tsconfig": "^4.10.1", "stable-hash-x": "^0.2.0" }, "peerDependencies": { "unrs-resolver": "^1.0.0" }, "optionalPeers": ["unrs-resolver"] }, "sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg=="],
|
||||
|
||||
@@ -474,19 +418,19 @@
|
||||
|
||||
"eslint-plugin-import": ["eslint-plugin-import@2.32.0", "", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", "array.prototype.findlastindex": "^1.2.6", "array.prototype.flat": "^1.3.3", "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.1", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA=="],
|
||||
|
||||
"eslint-plugin-import-newlines": ["eslint-plugin-import-newlines@1.4.1", "", { "peerDependencies": { "eslint": ">=6.0.0 < 10.0.0" }, "bin": { "import-linter": "lib/index.js" } }, "sha512-C9PQEJ4jS5tKoE9k0yY/5j4l1bxkxmVjrWvAFc1EToCnuQuwZGl1kS1JBlqYNF8svp0pnXLlkSdjByYa3JALcg=="],
|
||||
"eslint-plugin-import-newlines": ["eslint-plugin-import-newlines@2.0.0", "", { "peerDependencies": { "eslint": ">=10.0.0" }, "bin": { "import-linter": "lib/index.js" } }, "sha512-xKcuSkpQvkAHWCvAysqCk8GAD+rabLokiK4rmeJjCB+CQtGn6Ptgs909miphvN51JyZxOJWz4reGMsoHSbjbIg=="],
|
||||
|
||||
"eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA=="],
|
||||
|
||||
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA=="],
|
||||
|
||||
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg=="],
|
||||
"eslint-scope": ["eslint-scope@9.1.2", "", { "dependencies": { "@types/esrecurse": "^4.3.1", "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ=="],
|
||||
|
||||
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ=="],
|
||||
|
||||
"espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
|
||||
|
||||
"esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="],
|
||||
"esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
|
||||
|
||||
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
|
||||
|
||||
@@ -496,7 +440,7 @@
|
||||
|
||||
"esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
|
||||
|
||||
"expect-type": ["expect-type@1.2.2", "", {}, "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA=="],
|
||||
"expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
|
||||
|
||||
@@ -514,7 +458,7 @@
|
||||
|
||||
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
|
||||
|
||||
"flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
|
||||
"flatted": ["flatted@3.4.1", "", {}, "sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ=="],
|
||||
|
||||
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
|
||||
|
||||
@@ -526,9 +470,11 @@
|
||||
|
||||
"functions-have-names": ["functions-have-names@1.2.3", "", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="],
|
||||
|
||||
"generator-function": ["generator-function@2.0.1", "", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="],
|
||||
|
||||
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
|
||||
|
||||
"get-east-asian-width": ["get-east-asian-width@1.3.0", "", {}, "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ=="],
|
||||
"get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="],
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "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" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
|
||||
@@ -536,7 +482,7 @@
|
||||
|
||||
"get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="],
|
||||
|
||||
"get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="],
|
||||
"get-tsconfig": ["get-tsconfig@4.13.6", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw=="],
|
||||
|
||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
|
||||
|
||||
@@ -546,12 +492,10 @@
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
|
||||
|
||||
"gradient-string": ["gradient-string@2.0.2", "", { "dependencies": { "chalk": "^4.1.2", "tinygradient": "^1.1.5" } }, "sha512-rEDCuqUQ4tbD78TpzsMtt5OIf0cBCSDWSJtUDaF6JsAh+k0v9r++NzxNEG87oDZx9ZwGhD8DaezR2L/yrw0Jdw=="],
|
||||
"gradient-string": ["gradient-string@3.0.0", "", { "dependencies": { "chalk": "^5.3.0", "tinygradient": "^1.1.5" } }, "sha512-frdKI4Qi8Ihp4C6wZNB565de/THpIaw3DjP5ku87M+N9rNSGmPTjfkq61SdRXB7eCaL8O1hkKDvf6CDMtOzIAg=="],
|
||||
|
||||
"has-bigints": ["has-bigints@1.1.0", "", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="],
|
||||
|
||||
"has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
|
||||
|
||||
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="],
|
||||
|
||||
"has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="],
|
||||
@@ -566,9 +510,9 @@
|
||||
|
||||
"hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="],
|
||||
|
||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
"https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||
|
||||
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
|
||||
"ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
|
||||
|
||||
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
||||
|
||||
@@ -576,7 +520,7 @@
|
||||
|
||||
"ink": ["ink@6.2.0", "", { "dependencies": { "@alcalzone/ansi-tokenize": "^0.1.3", "ansi-escapes": "^7.0.0", "ansi-styles": "^6.2.1", "auto-bind": "^5.0.1", "chalk": "^5.3.0", "cli-boxes": "^3.0.0", "cli-cursor": "^4.0.0", "cli-truncate": "^4.0.0", "code-excerpt": "^4.0.0", "es-toolkit": "^1.22.0", "indent-string": "^5.0.0", "is-in-ci": "^1.0.0", "patch-console": "^2.0.0", "react-reconciler": "^0.32.0", "scheduler": "^0.23.0", "signal-exit": "^3.0.7", "slice-ansi": "^7.1.0", "stack-utils": "^2.0.6", "string-width": "^7.2.0", "type-fest": "^4.27.0", "widest-line": "^5.0.0", "wrap-ansi": "^9.0.0", "ws": "^8.18.0", "yoga-layout": "~3.2.1" }, "peerDependencies": { "@types/react": ">=19.0.0", "react": ">=19.0.0", "react-devtools-core": "^4.19.1" }, "optionalPeers": ["@types/react", "react-devtools-core"] }, "sha512-NQbNokT11cuxlIcCDfBMk1vEwaqc/cjTSqc4R4JugBO4BpWVe2B2A6ElC2koZQ9Vj91z0C40zid/jxOF2hJL9A=="],
|
||||
|
||||
"ink-gradient": ["ink-gradient@3.0.0", "", { "dependencies": { "@types/gradient-string": "^1.1.2", "gradient-string": "^2.0.2", "prop-types": "^15.8.1", "strip-ansi": "^7.1.0" }, "peerDependencies": { "ink": ">=4" } }, "sha512-OVyPBovBxE1tFcBhSamb+P1puqDP6pG3xFe2W9NiLgwUZd9RbcjBeR7twLbliUT9navrUstEf1ZcPKKvx71BsQ=="],
|
||||
"ink-gradient": ["ink-gradient@4.0.0", "", { "dependencies": { "@types/gradient-string": "^1.1.6", "gradient-string": "^3.0.0", "strip-ansi": "^7.1.2" }, "peerDependencies": { "ink": ">=6" } }, "sha512-Yx227CStr4DaXVkRAQPbBufSUTqe4a4FLOPVoypXZyae5h3A5jWyqZpTmAIbm7iiiqNYCkKIFBUPJM6nSICfxA=="],
|
||||
|
||||
"ink-select-input": ["ink-select-input@6.2.0", "", { "dependencies": { "figures": "^6.1.0", "to-rotated": "^1.0.0" }, "peerDependencies": { "ink": ">=5.0.0", "react": ">=18.0.0" } }, "sha512-304fZXxkpYxJ9si5lxRCaX01GNlmPBgOZumXXRnPYbHW/iI31cgQynqk2tRypGLOF1cMIwPUzL2LSm6q4I5rQQ=="],
|
||||
|
||||
@@ -606,7 +550,7 @@
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@4.0.0", "", {}, "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ=="],
|
||||
|
||||
"is-generator-function": ["is-generator-function@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "get-proto": "^1.0.0", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ=="],
|
||||
"is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="],
|
||||
|
||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
|
||||
|
||||
@@ -646,8 +590,6 @@
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
|
||||
|
||||
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
|
||||
|
||||
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
|
||||
|
||||
"json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
|
||||
@@ -664,12 +606,34 @@
|
||||
|
||||
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||
|
||||
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||
|
||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
||||
|
||||
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
|
||||
|
||||
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
|
||||
|
||||
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
|
||||
|
||||
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
|
||||
|
||||
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
|
||||
|
||||
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
|
||||
|
||||
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
|
||||
|
||||
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
|
||||
|
||||
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
|
||||
|
||||
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
|
||||
|
||||
"linkify-it": ["linkify-it@5.0.0", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ=="],
|
||||
|
||||
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
|
||||
|
||||
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
|
||||
|
||||
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
|
||||
|
||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
@@ -678,7 +642,7 @@
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
"markdown-it": ["markdown-it@14.1.0", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg=="],
|
||||
"markdown-it": ["markdown-it@14.1.1", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA=="],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||
|
||||
@@ -686,7 +650,7 @@
|
||||
|
||||
"mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="],
|
||||
|
||||
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
|
||||
"minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
|
||||
|
||||
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
|
||||
|
||||
@@ -694,10 +658,12 @@
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"napi-postinstall": ["napi-postinstall@0.3.3", "", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow=="],
|
||||
"napi-postinstall": ["napi-postinstall@0.3.4", "", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ=="],
|
||||
|
||||
"natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
|
||||
|
||||
"node-exports-info": ["node-exports-info@1.6.0", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw=="],
|
||||
|
||||
"node-releases": ["node-releases@2.0.36", "", {}, "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
@@ -728,8 +694,6 @@
|
||||
|
||||
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
|
||||
|
||||
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
|
||||
|
||||
"patch-console": ["patch-console@2.0.0", "", {}, "sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA=="],
|
||||
|
||||
"path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||
@@ -748,7 +712,7 @@
|
||||
|
||||
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
|
||||
|
||||
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
|
||||
|
||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
|
||||
|
||||
@@ -770,15 +734,13 @@
|
||||
|
||||
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="],
|
||||
|
||||
"resolve": ["resolve@2.0.0-next.5", "", { "dependencies": { "is-core-module": "^2.13.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA=="],
|
||||
|
||||
"resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||
"resolve": ["resolve@2.0.0-next.6", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA=="],
|
||||
|
||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||
|
||||
"restore-cursor": ["restore-cursor@4.0.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg=="],
|
||||
|
||||
"rollup": ["rollup@4.49.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.49.0", "@rollup/rollup-android-arm64": "4.49.0", "@rollup/rollup-darwin-arm64": "4.49.0", "@rollup/rollup-darwin-x64": "4.49.0", "@rollup/rollup-freebsd-arm64": "4.49.0", "@rollup/rollup-freebsd-x64": "4.49.0", "@rollup/rollup-linux-arm-gnueabihf": "4.49.0", "@rollup/rollup-linux-arm-musleabihf": "4.49.0", "@rollup/rollup-linux-arm64-gnu": "4.49.0", "@rollup/rollup-linux-arm64-musl": "4.49.0", "@rollup/rollup-linux-loongarch64-gnu": "4.49.0", "@rollup/rollup-linux-ppc64-gnu": "4.49.0", "@rollup/rollup-linux-riscv64-gnu": "4.49.0", "@rollup/rollup-linux-riscv64-musl": "4.49.0", "@rollup/rollup-linux-s390x-gnu": "4.49.0", "@rollup/rollup-linux-x64-gnu": "4.49.0", "@rollup/rollup-linux-x64-musl": "4.49.0", "@rollup/rollup-win32-arm64-msvc": "4.49.0", "@rollup/rollup-win32-ia32-msvc": "4.49.0", "@rollup/rollup-win32-x64-msvc": "4.49.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-3IVq0cGJ6H7fKXXEdVt+RcYvRCt8beYY9K1760wGQwSAHZcS9eot1zDG5axUbcp/kWRi5zKIIDX8MoKv/TzvZA=="],
|
||||
"rolldown": ["rolldown@1.0.0-rc.9", "", { "dependencies": { "@oxc-project/types": "=0.115.0", "@rolldown/pluginutils": "1.0.0-rc.9" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.9", "@rolldown/binding-darwin-arm64": "1.0.0-rc.9", "@rolldown/binding-darwin-x64": "1.0.0-rc.9", "@rolldown/binding-freebsd-x64": "1.0.0-rc.9", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.9", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.9", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.9", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.9", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.9", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.9", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.9", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.9", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.9", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.9", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q=="],
|
||||
|
||||
"safe-array-concat": ["safe-array-concat@1.1.3", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="],
|
||||
|
||||
@@ -824,7 +786,7 @@
|
||||
|
||||
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
|
||||
|
||||
"std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
|
||||
"std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="],
|
||||
|
||||
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
|
||||
|
||||
@@ -844,23 +806,19 @@
|
||||
|
||||
"strip-bom": ["strip-bom@3.0.0", "", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="],
|
||||
|
||||
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
|
||||
|
||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||
|
||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||
|
||||
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
|
||||
|
||||
"tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="],
|
||||
|
||||
"tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="],
|
||||
"tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
|
||||
|
||||
"tinygradient": ["tinygradient@1.1.5", "", { "dependencies": { "@types/tinycolor2": "^1.4.0", "tinycolor2": "^1.0.0" } }, "sha512-8nIfc2vgQ4TeLnk2lFj4tRLvvJwEfQuabdsmvDdQPT0xlk9TaNtpGd6nNRxXoK6vQhN6RSzj+Cnp5tTQmpxmbw=="],
|
||||
|
||||
"tinyrainbow": ["tinyrainbow@3.0.3", "", {}, "sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q=="],
|
||||
"tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="],
|
||||
|
||||
"to-rotated": ["to-rotated@1.0.0", "", {}, "sha512-KsEID8AfgUy+pxVRLsWp0VzCa69wxzUDZnzGbyIST/bcgcrMvTYoFBX/QORH4YApoD89EDuUovx4BTdpOn319Q=="],
|
||||
|
||||
@@ -886,13 +844,13 @@
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"typescript-eslint": ["typescript-eslint@8.56.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.56.1", "@typescript-eslint/parser": "8.56.1", "@typescript-eslint/typescript-estree": "8.56.1", "@typescript-eslint/utils": "8.56.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ=="],
|
||||
"typescript-eslint": ["typescript-eslint@8.57.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.57.0", "@typescript-eslint/parser": "8.57.0", "@typescript-eslint/typescript-estree": "8.57.0", "@typescript-eslint/utils": "8.57.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-W8GcigEMEeB07xEZol8oJ26rigm3+bfPHxHvwbYUlu1fUDsGuQ7Hiskx5xGW/xM4USc9Ephe3jtv7ZYPQntHeA=="],
|
||||
|
||||
"uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="],
|
||||
|
||||
"unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
|
||||
|
||||
"undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
|
||||
"undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="],
|
||||
|
||||
"unrs-resolver": ["unrs-resolver@1.11.1", "", { "dependencies": { "napi-postinstall": "^0.3.0" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.11.1", "@unrs/resolver-binding-android-arm64": "1.11.1", "@unrs/resolver-binding-darwin-arm64": "1.11.1", "@unrs/resolver-binding-darwin-x64": "1.11.1", "@unrs/resolver-binding-freebsd-x64": "1.11.1", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-musl": "1.11.1", "@unrs/resolver-binding-wasm32-wasi": "1.11.1", "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg=="],
|
||||
|
||||
@@ -900,9 +858,9 @@
|
||||
|
||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
|
||||
|
||||
"vite": ["vite@7.1.3", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-OOUi5zjkDxYrKhTV3V7iKsoS37VUM7v40+HuwEmcrsf11Cdx9y3DIr2Px6liIcZFwt3XSRpQvFpL3WVy7ApkGw=="],
|
||||
"vite": ["vite@8.0.0", "", { "dependencies": { "@oxc-project/runtime": "0.115.0", "lightningcss": "^1.32.0", "picomatch": "^4.0.3", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.9", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.0.0-alpha.31", "esbuild": "^0.27.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q=="],
|
||||
|
||||
"vitest": ["vitest@4.0.18", "", { "dependencies": { "@vitest/expect": "4.0.18", "@vitest/mocker": "4.0.18", "@vitest/pretty-format": "4.0.18", "@vitest/runner": "4.0.18", "@vitest/snapshot": "4.0.18", "@vitest/spy": "4.0.18", "@vitest/utils": "4.0.18", "es-module-lexer": "^1.7.0", "expect-type": "^1.2.2", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^3.10.0", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.0.18", "@vitest/browser-preview": "4.0.18", "@vitest/browser-webdriverio": "4.0.18", "@vitest/ui": "4.0.18", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ=="],
|
||||
"vitest": ["vitest@4.1.0", "", { "dependencies": { "@vitest/expect": "4.1.0", "@vitest/mocker": "4.1.0", "@vitest/pretty-format": "4.1.0", "@vitest/runner": "4.1.0", "@vitest/snapshot": "4.1.0", "@vitest/spy": "4.1.0", "@vitest/utils": "4.1.0", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.0.3", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.0", "@vitest/browser-preview": "4.1.0", "@vitest/browser-webdriverio": "4.1.0", "@vitest/ui": "4.1.0", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
@@ -912,7 +870,7 @@
|
||||
|
||||
"which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="],
|
||||
|
||||
"which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="],
|
||||
"which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="],
|
||||
|
||||
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
|
||||
|
||||
@@ -920,13 +878,13 @@
|
||||
|
||||
"word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="],
|
||||
"wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
|
||||
|
||||
"ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="],
|
||||
"ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="],
|
||||
|
||||
"yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
|
||||
|
||||
"yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="],
|
||||
"yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="],
|
||||
|
||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
|
||||
|
||||
@@ -940,47 +898,33 @@
|
||||
|
||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||
|
||||
"@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="],
|
||||
"@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.1", "", { "dependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" } }, "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||
|
||||
"@typescript-eslint/parser/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"@typescript-eslint/project-service/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"@typescript-eslint/type-utils/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||
|
||||
"@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
|
||||
|
||||
"cli-truncate/slice-ansi": ["slice-ansi@5.0.0", "", { "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" } }, "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ=="],
|
||||
|
||||
"eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
"eslint/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
|
||||
|
||||
"eslint/espree": ["espree@11.2.0", "", { "dependencies": { "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^5.0.1" } }, "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw=="],
|
||||
|
||||
"eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
||||
|
||||
"eslint-import-resolver-node/resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
|
||||
|
||||
"eslint-import-resolver-typescript/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
|
||||
"eslint-import-resolver-node/resolve": ["resolve@1.22.11", "", { "dependencies": { "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ=="],
|
||||
|
||||
"eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
||||
|
||||
"eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
||||
|
||||
"eslint-plugin-react-hooks/zod": ["zod@4.0.17", "", {}, "sha512-1PHjlYRevNxxdy2JZ8JcNAw7rX8V9P1AKkP+x/xZfxB0K5FYfuV+Ug6P/6NVSR2jHQ+FzDDoDHS04nYUsOIyLQ=="],
|
||||
"eslint-plugin-import/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
||||
|
||||
"gradient-string/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||
"eslint-plugin-react/minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
||||
|
||||
"ink-gradient/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
|
||||
|
||||
"is-bun-module/semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
|
||||
"is-bun-module/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||
|
||||
"react-devtools-core/ws": ["ws@7.5.10", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="],
|
||||
|
||||
@@ -990,32 +934,18 @@
|
||||
|
||||
"stack-utils/escape-string-regexp": ["escape-string-regexp@2.0.0", "", {}, "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w=="],
|
||||
|
||||
"string-width/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
|
||||
"typedoc/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
|
||||
|
||||
"typedoc/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
"eslint-plugin-import/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"vite/tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
|
||||
|
||||
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.1", "", {}, "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug=="],
|
||||
|
||||
"wrap-ansi/strip-ansi": ["strip-ansi@7.1.0", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="],
|
||||
|
||||
"eslint/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"gradient-string/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||
|
||||
"ink-gradient/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||
|
||||
"slice-ansi/is-fullwidth-code-point/get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="],
|
||||
|
||||
"string-width/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||
"eslint-plugin-react/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
|
||||
|
||||
"typedoc/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="],
|
||||
|
||||
"wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@6.1.0", "", {}, "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA=="],
|
||||
"eslint-plugin-import/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||
"eslint-plugin-react/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
|
||||
"typedoc/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||
}
|
||||
}
|
||||
|
||||
+9
-6
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ccstatusline",
|
||||
"version": "2.2.0",
|
||||
"version": "2.2.6",
|
||||
"description": "A customizable status line formatter for Claude Code CLI",
|
||||
"module": "src/ccstatusline.ts",
|
||||
"type": "module",
|
||||
@@ -16,25 +16,28 @@
|
||||
"postbuild": "bun run scripts/replace-version.ts",
|
||||
"example": "cat scripts/payload.example.json | bun start",
|
||||
"prepublishOnly": "bun run build",
|
||||
"lint": "bun tsc --noEmit && eslint . --config eslint.config.js --max-warnings=999999 --fix",
|
||||
"lint": "bun tsc --noEmit && eslint . --config eslint.config.js --max-warnings=0",
|
||||
"lint:fix": "bun tsc --noEmit && eslint . --config eslint.config.js --max-warnings=0 --fix",
|
||||
"docs": "typedoc",
|
||||
"docs:clean": "rm -rf docs"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.33.0",
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@stylistic/eslint-plugin": "^5.2.3",
|
||||
"@types/bun": "latest",
|
||||
"@types/pluralize": "^0.0.33",
|
||||
"@types/react": "^19.1.10",
|
||||
"chalk": "^5.5.0",
|
||||
"eslint": "^9.33.0",
|
||||
"eslint": "^10.0.0",
|
||||
"eslint-import-resolver-typescript": "^4.4.4",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-import-newlines": "^1.4.0",
|
||||
"eslint-plugin-import-newlines": "^2.0.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"globals": "^14.0.0",
|
||||
"https-proxy-agent": "^7.0.0",
|
||||
"ink": "6.2.0",
|
||||
"ink-gradient": "^3.0.0",
|
||||
"ink-gradient": "^4.0.0",
|
||||
"ink-select-input": "^6.2.0",
|
||||
"pluralize": "^8.0.0",
|
||||
"react": "^19.1.1",
|
||||
|
||||
@@ -3,6 +3,7 @@ import chalk from 'chalk';
|
||||
|
||||
import { runTUI } from './tui';
|
||||
import type {
|
||||
SkillsMetrics,
|
||||
SpeedMetrics,
|
||||
TokenMetrics
|
||||
} from './types';
|
||||
@@ -27,6 +28,10 @@ import {
|
||||
renderStatusLine
|
||||
} from './utils/renderer';
|
||||
import { advanceGlobalSeparatorIndex } from './utils/separator-index';
|
||||
import {
|
||||
getSkillsFilePath,
|
||||
getSkillsMetrics
|
||||
} from './utils/skills';
|
||||
import {
|
||||
getWidgetSpeedWindowSeconds,
|
||||
isWidgetSpeedWindowEnabled
|
||||
@@ -130,6 +135,11 @@ async function renderMultipleLines(data: StatusJSON) {
|
||||
windowedSpeedMetrics = speedMetricsCollection.windowed;
|
||||
}
|
||||
|
||||
let skillsMetrics: SkillsMetrics | null = null;
|
||||
if (data.session_id) {
|
||||
skillsMetrics = getSkillsMetrics(data.session_id);
|
||||
}
|
||||
|
||||
// Create render context
|
||||
const context: RenderContext = {
|
||||
data,
|
||||
@@ -138,6 +148,7 @@ async function renderMultipleLines(data: StatusJSON) {
|
||||
windowedSpeedMetrics,
|
||||
usageData,
|
||||
sessionDuration,
|
||||
skillsMetrics,
|
||||
isPreview: false
|
||||
};
|
||||
|
||||
@@ -213,10 +224,67 @@ function parseConfigArg(): string | undefined {
|
||||
return configPath;
|
||||
}
|
||||
|
||||
interface HookInput {
|
||||
session_id?: string;
|
||||
hook_event_name?: string;
|
||||
tool_name?: string;
|
||||
tool_input?: { skill?: string };
|
||||
prompt?: string;
|
||||
}
|
||||
|
||||
async function handleHook(): Promise<void> {
|
||||
const input = await readStdin();
|
||||
if (!input) {
|
||||
console.log('{}');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const data = JSON.parse(input) as HookInput;
|
||||
const sessionId = data.session_id;
|
||||
if (!sessionId) {
|
||||
console.log('{}');
|
||||
return;
|
||||
}
|
||||
|
||||
let skillName = '';
|
||||
if (data.hook_event_name === 'PreToolUse' && data.tool_name === 'Skill') {
|
||||
skillName = data.tool_input?.skill ?? '';
|
||||
} else if (data.hook_event_name === 'UserPromptSubmit') {
|
||||
const match = /^\/([a-zA-Z0-9_:-]+)/.exec(data.prompt ?? '');
|
||||
if (match) {
|
||||
skillName = match[1] ?? '';
|
||||
}
|
||||
}
|
||||
if (!skillName) {
|
||||
console.log('{}');
|
||||
return;
|
||||
}
|
||||
|
||||
const filePath = getSkillsFilePath(sessionId);
|
||||
const fs = await import('fs');
|
||||
const path = await import('path');
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
const entry = JSON.stringify({
|
||||
timestamp: new Date().toISOString(),
|
||||
session_id: sessionId,
|
||||
skill: skillName,
|
||||
source: data.hook_event_name
|
||||
});
|
||||
fs.appendFileSync(filePath, entry + '\n');
|
||||
} catch { /* ignore parse errors */ }
|
||||
console.log('{}');
|
||||
}
|
||||
|
||||
async function main() {
|
||||
// Parse --config before anything else
|
||||
initConfigPath(parseConfigArg());
|
||||
|
||||
// Handle --hook mode (cross-platform hook handler for widgets)
|
||||
if (process.argv.includes('--hook')) {
|
||||
await handleHook();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we're in a piped/non-TTY environment first
|
||||
if (!process.stdin.isTTY) {
|
||||
await ensureWindowsUtf8CodePage();
|
||||
|
||||
+48
-17
@@ -63,15 +63,47 @@ interface FlashMessage {
|
||||
color: 'green' | 'red';
|
||||
}
|
||||
|
||||
type AppScreen = 'main'
|
||||
| 'lines'
|
||||
| 'items'
|
||||
| 'colorLines'
|
||||
| 'colors'
|
||||
| 'terminalWidth'
|
||||
| 'terminalConfig'
|
||||
| 'globalOverrides'
|
||||
| 'confirm'
|
||||
| 'powerline'
|
||||
| 'install';
|
||||
|
||||
interface ConfirmDialogState {
|
||||
message: string;
|
||||
action: () => Promise<void>;
|
||||
cancelScreen?: Exclude<AppScreen, 'confirm'>;
|
||||
}
|
||||
|
||||
export function getConfirmCancelScreen(confirmDialog: ConfirmDialogState | null): Exclude<AppScreen, 'confirm'> {
|
||||
return confirmDialog?.cancelScreen ?? 'main';
|
||||
}
|
||||
|
||||
export function clearInstallMenuSelection(menuSelections: Record<string, number>): Record<string, number> {
|
||||
if (menuSelections.install === undefined) {
|
||||
return menuSelections;
|
||||
}
|
||||
|
||||
const next = { ...menuSelections };
|
||||
delete next.install;
|
||||
return next;
|
||||
}
|
||||
|
||||
export const App: React.FC = () => {
|
||||
const { exit } = useApp();
|
||||
const [settings, setSettings] = useState<Settings | null>(null);
|
||||
const [originalSettings, setOriginalSettings] = useState<Settings | null>(null);
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [screen, setScreen] = useState<'main' | 'lines' | 'items' | 'colorLines' | 'colors' | 'terminalWidth' | 'terminalConfig' | 'globalOverrides' | 'confirm' | 'powerline' | 'install'>('main');
|
||||
const [screen, setScreen] = useState<AppScreen>('main');
|
||||
const [selectedLine, setSelectedLine] = useState(0);
|
||||
const [menuSelections, setMenuSelections] = useState<Record<string, number>>({});
|
||||
const [confirmDialog, setConfirmDialog] = useState<{ message: string; action: () => Promise<void> } | null>(null);
|
||||
const [confirmDialog, setConfirmDialog] = useState<ConfirmDialogState | null>(null);
|
||||
const [isClaudeInstalled, setIsClaudeInstalled] = useState(false);
|
||||
const [terminalWidth, setTerminalWidth] = useState(process.stdout.columns || 80);
|
||||
const [powerlineFontStatus, setPowerlineFontStatus] = useState<PowerlineFontStatus>({ installed: false });
|
||||
@@ -163,6 +195,7 @@ export const App: React.FC = () => {
|
||||
|
||||
setConfirmDialog({
|
||||
message,
|
||||
cancelScreen: 'install',
|
||||
action: async () => {
|
||||
await installStatusLine(useBunx);
|
||||
setIsClaudeInstalled(true);
|
||||
@@ -176,13 +209,20 @@ export const App: React.FC = () => {
|
||||
}, []);
|
||||
|
||||
const handleNpxInstall = useCallback(() => {
|
||||
setMenuSelections(prev => ({ ...prev, install: 0 }));
|
||||
handleInstallSelection(CCSTATUSLINE_COMMANDS.NPM, 'npx', false);
|
||||
}, [handleInstallSelection]);
|
||||
|
||||
const handleBunxInstall = useCallback(() => {
|
||||
setMenuSelections(prev => ({ ...prev, install: 1 }));
|
||||
handleInstallSelection(CCSTATUSLINE_COMMANDS.BUNX, 'bunx', true);
|
||||
}, [handleInstallSelection]);
|
||||
|
||||
const handleInstallMenuCancel = useCallback(() => {
|
||||
setMenuSelections(clearInstallMenuSelection);
|
||||
setScreen('main');
|
||||
}, []);
|
||||
|
||||
if (!settings) {
|
||||
return <Text>Loading settings...</Text>;
|
||||
}
|
||||
@@ -308,20 +348,12 @@ export const App: React.FC = () => {
|
||||
<Box marginTop={1}>
|
||||
{screen === 'main' && (
|
||||
<MainMenu
|
||||
onSelect={(value) => {
|
||||
onSelect={(value, index) => {
|
||||
// Only persist menu selection if not exiting
|
||||
if (value !== 'save' && value !== 'exit') {
|
||||
const menuMap: Record<string, number> = {
|
||||
lines: 0,
|
||||
colors: 1,
|
||||
powerline: 2,
|
||||
terminalConfig: 3,
|
||||
globalOverrides: 4,
|
||||
install: 5,
|
||||
starGithub: hasChanges ? 8 : 7
|
||||
};
|
||||
setMenuSelections(prev => ({ ...prev, main: menuMap[value] ?? 0 }));
|
||||
setMenuSelections(prev => ({ ...prev, main: index }));
|
||||
}
|
||||
|
||||
void handleMainMenuSelect(value);
|
||||
}}
|
||||
isClaudeInstalled={isClaudeInstalled}
|
||||
@@ -448,7 +480,7 @@ export const App: React.FC = () => {
|
||||
message={confirmDialog.message}
|
||||
onConfirm={() => void confirmDialog.action()}
|
||||
onCancel={() => {
|
||||
setScreen('main');
|
||||
setScreen(getConfirmCancelScreen(confirmDialog));
|
||||
setConfirmDialog(null);
|
||||
}}
|
||||
/>
|
||||
@@ -459,9 +491,8 @@ export const App: React.FC = () => {
|
||||
existingStatusLine={existingStatusLine}
|
||||
onSelectNpx={handleNpxInstall}
|
||||
onSelectBunx={handleBunxInstall}
|
||||
onCancel={() => {
|
||||
setScreen('main');
|
||||
}}
|
||||
onCancel={handleInstallMenuCancel}
|
||||
initialSelection={menuSelections.install}
|
||||
/>
|
||||
)}
|
||||
{screen === 'powerline' && (
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import {
|
||||
describe,
|
||||
expect,
|
||||
it
|
||||
} from 'vitest';
|
||||
|
||||
import {
|
||||
clearInstallMenuSelection,
|
||||
getConfirmCancelScreen
|
||||
} from '../App';
|
||||
|
||||
describe('App confirm navigation helpers', () => {
|
||||
it('defaults confirmation cancel navigation to the main menu', () => {
|
||||
expect(getConfirmCancelScreen(null)).toBe('main');
|
||||
expect(getConfirmCancelScreen({
|
||||
message: 'Confirm install?',
|
||||
action: () => Promise.resolve()
|
||||
})).toBe('main');
|
||||
});
|
||||
|
||||
it('returns to the install menu when the confirm dialog requests it', () => {
|
||||
expect(getConfirmCancelScreen({
|
||||
message: 'Confirm install?',
|
||||
action: () => Promise.resolve(),
|
||||
cancelScreen: 'install'
|
||||
})).toBe('install');
|
||||
});
|
||||
|
||||
it('clears saved install selection when leaving the install menu', () => {
|
||||
expect(clearInstallMenuSelection({
|
||||
main: 5,
|
||||
install: 1
|
||||
})).toEqual({ main: 5 });
|
||||
|
||||
const menuSelections = { main: 5 };
|
||||
|
||||
expect(clearInstallMenuSelection(menuSelections)).toBe(menuSelections);
|
||||
});
|
||||
});
|
||||
@@ -3,7 +3,12 @@ import {
|
||||
Text,
|
||||
useInput
|
||||
} from 'ink';
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
List,
|
||||
type ListEntry
|
||||
} from './List';
|
||||
|
||||
export interface ConfirmDialogProps {
|
||||
message?: string;
|
||||
@@ -12,52 +17,57 @@ export interface ConfirmDialogProps {
|
||||
inline?: boolean;
|
||||
}
|
||||
|
||||
export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({ message, onConfirm, onCancel, inline = false }) => {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0); // Default to "Yes"
|
||||
const CONFIRM_OPTIONS: ListEntry<boolean>[] = [
|
||||
{
|
||||
label: 'Yes',
|
||||
value: true
|
||||
},
|
||||
{
|
||||
label: 'No',
|
||||
value: false
|
||||
}
|
||||
];
|
||||
|
||||
useInput((input, key) => {
|
||||
if (key.upArrow) {
|
||||
setSelectedIndex(Math.max(0, selectedIndex - 1));
|
||||
} else if (key.downArrow) {
|
||||
setSelectedIndex(Math.min(1, selectedIndex + 1));
|
||||
} else if (key.return) {
|
||||
if (selectedIndex === 0) {
|
||||
onConfirm();
|
||||
} else {
|
||||
onCancel();
|
||||
}
|
||||
} else if (key.escape) {
|
||||
export const ConfirmDialog: React.FC<ConfirmDialogProps> = ({ message, onConfirm, onCancel, inline = false }) => {
|
||||
useInput((_, key) => {
|
||||
if (key.escape) {
|
||||
onCancel();
|
||||
}
|
||||
});
|
||||
|
||||
const renderOptions = () => {
|
||||
const yesStyle = selectedIndex === 0 ? { color: 'cyan' } : {};
|
||||
const noStyle = selectedIndex === 1 ? { color: 'cyan' } : {};
|
||||
|
||||
return (
|
||||
<Box flexDirection='column'>
|
||||
<Text {...yesStyle}>
|
||||
{selectedIndex === 0 ? '▶ ' : ' '}
|
||||
Yes
|
||||
</Text>
|
||||
<Text {...noStyle}>
|
||||
{selectedIndex === 1 ? '▶ ' : ' '}
|
||||
No
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
if (inline) {
|
||||
return renderOptions();
|
||||
return (
|
||||
<List
|
||||
items={CONFIRM_OPTIONS}
|
||||
onSelect={(confirmed) => {
|
||||
if (confirmed) {
|
||||
onConfirm();
|
||||
return;
|
||||
}
|
||||
|
||||
onCancel();
|
||||
}}
|
||||
color='cyan'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box flexDirection='column'>
|
||||
<Text>{message}</Text>
|
||||
<Box marginTop={1}>
|
||||
{renderOptions()}
|
||||
<List
|
||||
items={CONFIRM_OPTIONS}
|
||||
onSelect={(confirmed) => {
|
||||
if (confirmed) {
|
||||
onConfirm();
|
||||
return;
|
||||
}
|
||||
|
||||
onCancel();
|
||||
}}
|
||||
color='cyan'
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -3,16 +3,19 @@ import {
|
||||
Text,
|
||||
useInput
|
||||
} from 'ink';
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import { getClaudeSettingsPath } from '../../utils/claude-settings';
|
||||
|
||||
import { List } from './List';
|
||||
|
||||
export interface InstallMenuProps {
|
||||
bunxAvailable: boolean;
|
||||
existingStatusLine: string | null;
|
||||
onSelectNpx: () => void;
|
||||
onSelectBunx: () => void;
|
||||
onCancel: () => void;
|
||||
initialSelection?: number;
|
||||
}
|
||||
|
||||
export const InstallMenu: React.FC<InstallMenuProps> = ({
|
||||
@@ -20,39 +23,44 @@ export const InstallMenu: React.FC<InstallMenuProps> = ({
|
||||
existingStatusLine,
|
||||
onSelectNpx,
|
||||
onSelectBunx,
|
||||
onCancel
|
||||
onCancel,
|
||||
initialSelection = 0
|
||||
}) => {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const maxIndex = 2; // npx, bunx (if available), and back
|
||||
|
||||
useInput((input, key) => {
|
||||
useInput((_, key) => {
|
||||
if (key.escape) {
|
||||
onCancel();
|
||||
} else if (key.upArrow) {
|
||||
if (selectedIndex === 2) {
|
||||
setSelectedIndex(bunxAvailable ? 1 : 0); // Skip bunx if not available
|
||||
} else {
|
||||
setSelectedIndex(Math.max(0, selectedIndex - 1));
|
||||
}
|
||||
} else if (key.downArrow) {
|
||||
if (selectedIndex === 0) {
|
||||
setSelectedIndex(bunxAvailable ? 1 : 2); // Skip bunx if not available
|
||||
} else if (selectedIndex === 1 && bunxAvailable) {
|
||||
setSelectedIndex(2);
|
||||
} else {
|
||||
setSelectedIndex(Math.min(maxIndex, selectedIndex + 1));
|
||||
}
|
||||
} else if (key.return) {
|
||||
if (selectedIndex === 0) {
|
||||
onSelectNpx();
|
||||
} else if (selectedIndex === 1 && bunxAvailable) {
|
||||
onSelectBunx();
|
||||
} else if (selectedIndex === 2) {
|
||||
onCancel();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function onSelect(value: string) {
|
||||
switch (value) {
|
||||
case 'npx':
|
||||
onSelectNpx();
|
||||
break;
|
||||
case 'bunx':
|
||||
if (bunxAvailable) {
|
||||
onSelectBunx();
|
||||
}
|
||||
break;
|
||||
case 'back':
|
||||
onCancel();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const listItems = [
|
||||
{
|
||||
label: 'npx - Node Package Execute',
|
||||
value: 'npx'
|
||||
},
|
||||
{
|
||||
label: 'bunx - Bun Package Execute',
|
||||
sublabel: bunxAvailable ? undefined : '(not installed)',
|
||||
value: 'bunx',
|
||||
disabled: !bunxAvailable
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<Box flexDirection='column'>
|
||||
<Text bold>Install ccstatusline to Claude Code</Text>
|
||||
@@ -71,29 +79,21 @@ export const InstallMenu: React.FC<InstallMenuProps> = ({
|
||||
<Text dimColor>Select package manager to use:</Text>
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1} flexDirection='column'>
|
||||
<Box>
|
||||
<Text color={selectedIndex === 0 ? 'blue' : undefined}>
|
||||
{selectedIndex === 0 ? '▶ ' : ' '}
|
||||
npx - Node Package Execute
|
||||
</Text>
|
||||
</Box>
|
||||
<List
|
||||
color='blue'
|
||||
marginTop={1}
|
||||
items={listItems}
|
||||
onSelect={(line) => {
|
||||
if (line === 'back') {
|
||||
onCancel();
|
||||
return;
|
||||
}
|
||||
|
||||
<Box>
|
||||
<Text color={selectedIndex === 1 && bunxAvailable ? 'blue' : undefined} dimColor={!bunxAvailable}>
|
||||
{selectedIndex === 1 && bunxAvailable ? '▶ ' : ' '}
|
||||
bunx - Bun Package Execute
|
||||
{!bunxAvailable && ' (not installed)'}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text color={selectedIndex === 2 ? 'blue' : undefined}>
|
||||
{selectedIndex === 2 ? '▶ ' : ' '}
|
||||
← Back
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
onSelect(line);
|
||||
}}
|
||||
initialSelection={initialSelection}
|
||||
showBackButton={true}
|
||||
/>
|
||||
|
||||
<Box marginTop={2}>
|
||||
<Text dimColor>
|
||||
|
||||
@@ -94,21 +94,12 @@ export const ItemsEditor: React.FC<ItemsEditorProps> = ({ widgets, onUpdate, onB
|
||||
setCustomEditorWidget(null);
|
||||
};
|
||||
|
||||
const shouldShowCustomKeybind = (widget: WidgetItem, keybind: CustomKeybind): boolean => {
|
||||
if (keybind.action !== 'toggle-invert') {
|
||||
return true;
|
||||
}
|
||||
|
||||
const mode = widget.metadata?.display;
|
||||
return mode === 'progress' || mode === 'progress-short';
|
||||
};
|
||||
|
||||
const getVisibleCustomKeybinds = (widgetImpl: Widget, widget: WidgetItem): CustomKeybind[] => {
|
||||
const getCustomKeybindsForWidget = (widgetImpl: Widget, widget: WidgetItem): CustomKeybind[] => {
|
||||
if (!widgetImpl.getCustomKeybinds) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return widgetImpl.getCustomKeybinds().filter(keybind => shouldShowCustomKeybind(widget, keybind));
|
||||
return widgetImpl.getCustomKeybinds(widget);
|
||||
};
|
||||
|
||||
const openWidgetPicker = (action: WidgetPickerAction) => {
|
||||
@@ -208,7 +199,7 @@ export const ItemsEditor: React.FC<ItemsEditorProps> = ({ widgets, onUpdate, onB
|
||||
setMoveMode,
|
||||
setShowClearConfirm,
|
||||
openWidgetPicker,
|
||||
getVisibleCustomKeybinds,
|
||||
getCustomKeybindsForWidget,
|
||||
setCustomEditorWidget
|
||||
});
|
||||
});
|
||||
@@ -271,7 +262,7 @@ export const ItemsEditor: React.FC<ItemsEditorProps> = ({ widgets, onUpdate, onB
|
||||
if (widgetImpl) {
|
||||
canToggleRaw = widgetImpl.supportsRawValue();
|
||||
// Get custom keybinds from the widget
|
||||
customKeybinds = getVisibleCustomKeybinds(widgetImpl, currentWidget);
|
||||
customKeybinds = getCustomKeybindsForWidget(widgetImpl, currentWidget);
|
||||
} else {
|
||||
canToggleRaw = false;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import type { Settings } from '../../types/Settings';
|
||||
import type { WidgetItem } from '../../types/Widget';
|
||||
|
||||
import { ConfirmDialog } from './ConfirmDialog';
|
||||
import { List } from './List';
|
||||
|
||||
interface LineSelectorProps {
|
||||
lines: WidgetItem[][];
|
||||
@@ -47,6 +48,10 @@ const LineSelector: React.FC<LineSelectorProps> = ({
|
||||
setLocalLines(lines);
|
||||
}, [lines]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedIndex(initialSelection);
|
||||
}, [initialSelection]);
|
||||
|
||||
const selectedLine = useMemo(
|
||||
() => localLines[selectedIndex],
|
||||
[localLines, selectedIndex]
|
||||
@@ -60,7 +65,7 @@ const LineSelector: React.FC<LineSelectorProps> = ({
|
||||
};
|
||||
|
||||
const deleteLine = (lineIndex: number) => {
|
||||
// Don't allow deleting the last remaining line
|
||||
// Don't allow deleting the last remaining line
|
||||
if (localLines.length <= 1) {
|
||||
return;
|
||||
}
|
||||
@@ -125,7 +130,7 @@ const LineSelector: React.FC<LineSelectorProps> = ({
|
||||
}
|
||||
return;
|
||||
case 'd':
|
||||
if (allowEditing && localLines.length > 1) {
|
||||
if (allowEditing && localLines.length > 1 && selectedIndex < localLines.length) {
|
||||
setShowDeleteDialog(true);
|
||||
}
|
||||
return;
|
||||
@@ -138,16 +143,6 @@ const LineSelector: React.FC<LineSelectorProps> = ({
|
||||
|
||||
if (key.escape) {
|
||||
onBack();
|
||||
} else if (key.upArrow) {
|
||||
setSelectedIndex(Math.max(0, selectedIndex - 1));
|
||||
} else if (key.downArrow) {
|
||||
setSelectedIndex(Math.min(localLines.length, selectedIndex + 1));
|
||||
} else if (key.return) {
|
||||
if (selectedIndex === localLines.length) {
|
||||
onBack();
|
||||
} else {
|
||||
onSelect(selectedIndex);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -197,7 +192,6 @@ const LineSelector: React.FC<LineSelectorProps> = ({
|
||||
<Text>
|
||||
<Text>
|
||||
☰ Line
|
||||
{' '}
|
||||
{selectedIndex + 1}
|
||||
</Text>
|
||||
{' '}
|
||||
@@ -228,6 +222,12 @@ const LineSelector: React.FC<LineSelectorProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
const lineItems = localLines.map((line, index) => ({
|
||||
label: `☰ Line ${index + 1}`,
|
||||
sublabel: `(${line.length > 0 ? pluralize('widget', line.length, true) : 'empty'})`,
|
||||
value: index
|
||||
}));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box flexDirection='column'>
|
||||
@@ -253,44 +253,55 @@ const LineSelector: React.FC<LineSelectorProps> = ({
|
||||
</Text>
|
||||
)}
|
||||
|
||||
<Box marginTop={1} flexDirection='column'>
|
||||
{localLines.map((line, index) => {
|
||||
const isSelected = selectedIndex === index;
|
||||
const suffix = line.length
|
||||
? pluralize('widget', line.length, true)
|
||||
: 'empty';
|
||||
{moveMode ? (
|
||||
<Box marginTop={1} flexDirection='column'>
|
||||
{localLines.map((line, index) => {
|
||||
const isSelected = selectedIndex === index;
|
||||
const suffix = line.length
|
||||
? pluralize('widget', line.length, true)
|
||||
: 'empty';
|
||||
|
||||
return (
|
||||
<Box key={index}>
|
||||
<Text color={isSelected ? (moveMode ? 'blue' : 'green') : undefined}>
|
||||
<Text>{isSelected ? (moveMode ? '◆ ' : '▶ ') : ' '}</Text>
|
||||
<Text>
|
||||
return (
|
||||
<Box key={index}>
|
||||
<Text color={isSelected ? 'blue' : undefined}>
|
||||
<Text>{isSelected ? '◆ ' : ' '}</Text>
|
||||
<Text>
|
||||
☰ Line
|
||||
<Text>
|
||||
☰ Line
|
||||
{' '}
|
||||
{index + 1}
|
||||
</Text>
|
||||
{' '}
|
||||
{index + 1}
|
||||
</Text>
|
||||
{' '}
|
||||
<Text dimColor={!isSelected}>
|
||||
(
|
||||
{suffix}
|
||||
)
|
||||
<Text dimColor={!isSelected}>
|
||||
(
|
||||
{suffix}
|
||||
)
|
||||
</Text>
|
||||
</Text>
|
||||
</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
) : (
|
||||
<List
|
||||
marginTop={1}
|
||||
items={lineItems}
|
||||
onSelect={(line) => {
|
||||
if (line === 'back') {
|
||||
onBack();
|
||||
return;
|
||||
}
|
||||
|
||||
{!moveMode && (
|
||||
<Box marginTop={1}>
|
||||
<Text color={selectedIndex === localLines.length ? 'green' : undefined}>
|
||||
{selectedIndex === localLines.length ? '▶ ' : ' '}
|
||||
← Back
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
onSelect(line);
|
||||
}}
|
||||
onSelectionChange={(_, index) => {
|
||||
setSelectedIndex(index);
|
||||
}}
|
||||
initialSelection={selectedIndex}
|
||||
showBackButton={true}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
import type { ForegroundColorName } from 'chalk';
|
||||
import {
|
||||
Box,
|
||||
Text,
|
||||
useInput,
|
||||
type BoxProps
|
||||
} from 'ink';
|
||||
import {
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
type PropsWithChildren
|
||||
} from 'react';
|
||||
|
||||
export interface ListEntry<V = string | number> {
|
||||
label: string;
|
||||
sublabel?: string;
|
||||
disabled?: boolean;
|
||||
description?: string;
|
||||
value: V;
|
||||
props?: BoxProps;
|
||||
}
|
||||
|
||||
interface ListProps<V = string | number> extends BoxProps {
|
||||
items: (ListEntry<V> | '-')[];
|
||||
onSelect: (value: V | 'back', index: number) => void;
|
||||
onSelectionChange?: (value: V | 'back', index: number) => void;
|
||||
initialSelection?: number;
|
||||
showBackButton?: boolean;
|
||||
color?: ForegroundColorName;
|
||||
wrapNavigation?: boolean;
|
||||
}
|
||||
|
||||
export function List<V = string | number>({
|
||||
items,
|
||||
onSelect,
|
||||
onSelectionChange,
|
||||
initialSelection = 0,
|
||||
showBackButton,
|
||||
color,
|
||||
wrapNavigation = false,
|
||||
...boxProps
|
||||
}: ListProps<V>) {
|
||||
const [selectedIndex, setSelectedIndex] = useState(initialSelection);
|
||||
const latestOnSelectionChangeRef = useRef(onSelectionChange);
|
||||
|
||||
const _items = useMemo(() => {
|
||||
if (showBackButton) {
|
||||
return [...items, '-' as const, { label: '← Back', value: 'back' as V }];
|
||||
}
|
||||
return items;
|
||||
}, [items, showBackButton]);
|
||||
|
||||
const selectableItems = _items.filter(item => item !== '-' && !item.disabled) as ListEntry<V>[];
|
||||
const selectedItem = selectableItems[selectedIndex];
|
||||
const selectedValue = selectedItem?.value;
|
||||
const actualIndex = _items.findIndex(item => item === selectedItem);
|
||||
|
||||
useEffect(() => {
|
||||
latestOnSelectionChangeRef.current = onSelectionChange;
|
||||
}, [onSelectionChange]);
|
||||
|
||||
useEffect(() => {
|
||||
const maxIndex = Math.max(selectableItems.length - 1, 0);
|
||||
setSelectedIndex(Math.min(initialSelection, maxIndex));
|
||||
}, [initialSelection, selectableItems.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedValue !== undefined) {
|
||||
latestOnSelectionChangeRef.current?.(selectedValue, selectedIndex);
|
||||
}
|
||||
}, [selectedIndex, selectedValue]);
|
||||
|
||||
useInput((_, key) => {
|
||||
if (key.upArrow) {
|
||||
const prev = selectedIndex - 1;
|
||||
const prevIndex = prev < 0
|
||||
? (wrapNavigation ? selectableItems.length - 1 : 0)
|
||||
: prev;
|
||||
|
||||
setSelectedIndex(prevIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.downArrow) {
|
||||
const next = selectedIndex + 1;
|
||||
const nextIndex = next > selectableItems.length - 1
|
||||
? (wrapNavigation ? 0 : selectableItems.length - 1)
|
||||
: next;
|
||||
|
||||
setSelectedIndex(nextIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.return && selectedItem) {
|
||||
onSelect(selectedItem.value, selectedIndex);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Box flexDirection='column' {...boxProps}>
|
||||
{_items.map((item, index) => {
|
||||
if (item === '-') {
|
||||
return <ListSeparator key={index} />;
|
||||
}
|
||||
|
||||
const isSelected = index === actualIndex;
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
key={index}
|
||||
isSelected={isSelected}
|
||||
color={color}
|
||||
disabled={item.disabled}
|
||||
{...item.props}
|
||||
>
|
||||
<Text>
|
||||
<Text>
|
||||
{item.label}
|
||||
</Text>
|
||||
{item.sublabel && (
|
||||
<Text dimColor={!isSelected}>
|
||||
{' '}
|
||||
{item.sublabel}
|
||||
</Text>
|
||||
)}
|
||||
</Text>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
|
||||
{selectedItem?.description && (
|
||||
<Box marginTop={1} paddingLeft={2}>
|
||||
<Text dimColor wrap='wrap'>
|
||||
{selectedItem.description}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
interface ListItemProps extends PropsWithChildren, BoxProps {
|
||||
isSelected: boolean;
|
||||
color?: ForegroundColorName;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function ListItem({
|
||||
children,
|
||||
isSelected,
|
||||
color = 'green',
|
||||
disabled,
|
||||
...boxProps
|
||||
}: ListItemProps) {
|
||||
return (
|
||||
<Box {...boxProps}>
|
||||
<Text color={isSelected ? color : undefined} dimColor={disabled}>
|
||||
<Text>{isSelected ? '▶ ' : ' '}</Text>
|
||||
<Text>{children}</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export function ListSeparator() {
|
||||
return <Text> </Text>;
|
||||
}
|
||||
+106
-89
@@ -1,13 +1,14 @@
|
||||
import {
|
||||
Box,
|
||||
Text,
|
||||
useInput
|
||||
Text
|
||||
} from 'ink';
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
|
||||
import type { Settings } from '../../types/Settings';
|
||||
import { type PowerlineFontStatus } from '../../utils/powerline';
|
||||
|
||||
import { List } from './List';
|
||||
|
||||
export type MainMenuOption = 'lines'
|
||||
| 'colors'
|
||||
| 'powerline'
|
||||
@@ -19,7 +20,7 @@ export type MainMenuOption = 'lines'
|
||||
| 'exit';
|
||||
|
||||
export interface MainMenuProps {
|
||||
onSelect: (value: MainMenuOption) => void;
|
||||
onSelect: (value: MainMenuOption, index: number) => void;
|
||||
isClaudeInstalled: boolean;
|
||||
hasChanges: boolean;
|
||||
initialSelection?: number;
|
||||
@@ -28,111 +29,127 @@ export interface MainMenuProps {
|
||||
previewIsTruncated?: boolean;
|
||||
}
|
||||
|
||||
export const MainMenu: React.FC<MainMenuProps> = ({ onSelect, isClaudeInstalled, hasChanges, initialSelection = 0, powerlineFontStatus, settings, previewIsTruncated }) => {
|
||||
const [selectedIndex, setSelectedIndex] = useState(initialSelection);
|
||||
|
||||
export const MainMenu: React.FC<MainMenuProps> = ({
|
||||
onSelect,
|
||||
isClaudeInstalled,
|
||||
hasChanges,
|
||||
initialSelection = 0,
|
||||
powerlineFontStatus,
|
||||
settings,
|
||||
previewIsTruncated
|
||||
}) => {
|
||||
// Build menu structure with visual gaps
|
||||
const menuItems = [
|
||||
{ label: '📝 Edit Lines', value: 'lines', selectable: true },
|
||||
{ label: '🎨 Edit Colors', value: 'colors', selectable: true },
|
||||
{ label: '⚡ Powerline Setup', value: 'powerline', selectable: true },
|
||||
{ label: '', value: '_gap1', selectable: false }, // Visual gap
|
||||
{ label: '💻 Terminal Options', value: 'terminalConfig', selectable: true },
|
||||
{ label: '🌐 Global Overrides', value: 'globalOverrides', selectable: true },
|
||||
{ label: '', value: '_gap2', selectable: false }, // Visual gap
|
||||
{ label: isClaudeInstalled ? '🔌 Uninstall from Claude Code' : '📦 Install to Claude Code', value: 'install', selectable: true }
|
||||
const menuItems: ({
|
||||
label: string;
|
||||
value: MainMenuOption;
|
||||
description: string;
|
||||
} | '-')[] = [
|
||||
{
|
||||
label: '📝 Edit Lines',
|
||||
value: 'lines',
|
||||
description:
|
||||
'Configure any number of status lines with various widgets like model info, git status, and token usage'
|
||||
},
|
||||
{
|
||||
label: '🎨 Edit Colors',
|
||||
value: 'colors',
|
||||
description:
|
||||
'Customize colors for each widget including foreground, background, and bold styling'
|
||||
},
|
||||
{
|
||||
label: '⚡ Powerline Setup',
|
||||
value: 'powerline',
|
||||
description:
|
||||
'Install Powerline fonts for enhanced visual separators and symbols in your status line'
|
||||
},
|
||||
'-' as const,
|
||||
{
|
||||
label: '💻 Terminal Options',
|
||||
value: 'terminalConfig',
|
||||
description: 'Configure terminal-specific settings for optimal display'
|
||||
},
|
||||
{
|
||||
label: '🌐 Global Overrides',
|
||||
value: 'globalOverrides',
|
||||
description:
|
||||
'Set global padding, separators, and color overrides that apply to all widgets'
|
||||
},
|
||||
'-' as const,
|
||||
{
|
||||
label: isClaudeInstalled
|
||||
? '🔌 Uninstall from Claude Code'
|
||||
: '📦 Install to Claude Code',
|
||||
value: 'install',
|
||||
description: isClaudeInstalled
|
||||
? 'Remove ccstatusline from your Claude Code settings'
|
||||
: 'Add ccstatusline to your Claude Code settings for automatic status line rendering'
|
||||
}
|
||||
];
|
||||
|
||||
if (hasChanges) {
|
||||
menuItems.push(
|
||||
{ label: '💾 Save & Exit', value: 'save', selectable: true },
|
||||
{ label: '❌ Exit without saving', value: 'exit', selectable: true },
|
||||
{ label: '', value: '_gap3', selectable: false }, // Visual gap
|
||||
{ label: '⭐ Like ccstatusline? Star us on GitHub', value: 'starGithub', selectable: true }
|
||||
{
|
||||
label: '💾 Save & Exit',
|
||||
value: 'save',
|
||||
description: 'Save all changes and exit the configuration tool'
|
||||
},
|
||||
{
|
||||
label: '❌ Exit without saving',
|
||||
value: 'exit',
|
||||
description: 'Exit without saving your changes'
|
||||
},
|
||||
'-' as const,
|
||||
{
|
||||
label: '⭐ Like ccstatusline? Star us on GitHub',
|
||||
value: 'starGithub',
|
||||
description: 'Open the ccstatusline GitHub repository in your browser so you can star the project'
|
||||
}
|
||||
);
|
||||
} else {
|
||||
menuItems.push(
|
||||
{ label: '🚪 Exit', value: 'exit', selectable: true },
|
||||
{ label: '', value: '_gap3', selectable: false }, // Visual gap
|
||||
{ label: '⭐ Like ccstatusline? Star us on GitHub', value: 'starGithub', selectable: true }
|
||||
{
|
||||
label: '🚪 Exit',
|
||||
value: 'exit',
|
||||
description: 'Exit the configuration tool'
|
||||
},
|
||||
'-' as const,
|
||||
{
|
||||
label: '⭐ Like ccstatusline? Star us on GitHub',
|
||||
value: 'starGithub',
|
||||
description: 'Open the ccstatusline GitHub repository in your browser so you can star the project'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Get only selectable items for navigation
|
||||
const selectableItems = menuItems.filter(item => item.selectable);
|
||||
|
||||
useInput((input, key) => {
|
||||
if (key.upArrow) {
|
||||
setSelectedIndex(Math.max(0, selectedIndex - 1));
|
||||
} else if (key.downArrow) {
|
||||
setSelectedIndex(Math.min(selectableItems.length - 1, selectedIndex + 1));
|
||||
} else if (key.return) {
|
||||
const item = selectableItems[selectedIndex];
|
||||
if (item) {
|
||||
// Since we filtered by selectable: true, value is guaranteed to be MainMenuOption
|
||||
onSelect(item.value as MainMenuOption);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Get description for selected item
|
||||
const getDescription = (value: string): string => {
|
||||
const descriptions: Record<string, string> = {
|
||||
lines: 'Configure any number of status lines with various widgets like model info, git status, and token usage',
|
||||
colors: 'Customize colors for each widget including foreground, background, and bold styling',
|
||||
powerline: 'Install Powerline fonts for enhanced visual separators and symbols in your status line',
|
||||
globalOverrides: 'Set global padding, separators, and color overrides that apply to all widgets',
|
||||
install: isClaudeInstalled
|
||||
? 'Remove ccstatusline from your Claude Code settings'
|
||||
: 'Add ccstatusline to your Claude Code settings for automatic status line rendering',
|
||||
terminalConfig: 'Configure terminal-specific settings for optimal display',
|
||||
starGithub: 'Open the ccstatusline GitHub repository in your browser so you can star the project',
|
||||
save: 'Save all changes and exit the configuration tool',
|
||||
exit: hasChanges
|
||||
? 'Exit without saving your changes'
|
||||
: 'Exit the configuration tool'
|
||||
};
|
||||
return descriptions[value] ?? '';
|
||||
};
|
||||
|
||||
const selectedItem = selectableItems[selectedIndex];
|
||||
const description = selectedItem ? getDescription(selectedItem.value) : '';
|
||||
|
||||
// Check if we should show the truncation warning
|
||||
const showTruncationWarning = previewIsTruncated && settings?.flexMode === 'full-minus-40';
|
||||
const showTruncationWarning
|
||||
= previewIsTruncated && settings?.flexMode === 'full-minus-40';
|
||||
|
||||
return (
|
||||
<Box flexDirection='column'>
|
||||
{showTruncationWarning && (
|
||||
<Box marginBottom={1}>
|
||||
<Text color='yellow'>⚠ Some lines are truncated, see Terminal Options → Terminal Width for info</Text>
|
||||
<Text color='yellow'>
|
||||
⚠ Some lines are truncated, see Terminal Options → Terminal Width
|
||||
for info
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Text bold>Main Menu</Text>
|
||||
<Box marginTop={1} flexDirection='column'>
|
||||
{menuItems.map((item, idx) => {
|
||||
if (!item.selectable && item.value.startsWith('_gap')) {
|
||||
return <Text key={item.value}> </Text>;
|
||||
}
|
||||
const selectableIdx = selectableItems.indexOf(item);
|
||||
const isSelected = selectableIdx === selectedIndex;
|
||||
|
||||
return (
|
||||
<Text
|
||||
key={item.value}
|
||||
color={isSelected ? 'green' : undefined}
|
||||
>
|
||||
{isSelected ? '▶ ' : ' '}
|
||||
{item.label}
|
||||
</Text>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
{description && (
|
||||
<Box marginTop={1} paddingLeft={2}>
|
||||
<Text dimColor wrap='wrap'>{description}</Text>
|
||||
</Box>
|
||||
)}
|
||||
<Text bold>Main Menu</Text>
|
||||
|
||||
<List
|
||||
items={menuItems}
|
||||
marginTop={1}
|
||||
onSelect={(value, index) => {
|
||||
if (value === 'back') {
|
||||
return;
|
||||
}
|
||||
|
||||
onSelect(value, index);
|
||||
}}
|
||||
initialSelection={initialSelection}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -6,14 +6,139 @@ import {
|
||||
import * as os from 'os';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import type { PowerlineConfig } from '../../types/PowerlineConfig';
|
||||
import type { Settings } from '../../types/Settings';
|
||||
import { type PowerlineFontStatus } from '../../utils/powerline';
|
||||
import { buildEnabledPowerlineSettings } from '../../utils/powerline-settings';
|
||||
|
||||
import { ConfirmDialog } from './ConfirmDialog';
|
||||
import {
|
||||
List,
|
||||
type ListEntry
|
||||
} from './List';
|
||||
import { PowerlineSeparatorEditor } from './PowerlineSeparatorEditor';
|
||||
import { PowerlineThemeSelector } from './PowerlineThemeSelector';
|
||||
|
||||
type PowerlineMenuValue = 'separator' | 'startCap' | 'endCap' | 'themes';
|
||||
type Screen = 'menu' | PowerlineMenuValue;
|
||||
const POWERLINE_MENU_LABEL_WIDTH = 11;
|
||||
|
||||
function formatPowerlineMenuLabel(label: string): string {
|
||||
return label.padEnd(POWERLINE_MENU_LABEL_WIDTH, ' ');
|
||||
}
|
||||
|
||||
export function getSeparatorDisplay(powerlineConfig: PowerlineConfig): string {
|
||||
const seps = powerlineConfig.separators;
|
||||
|
||||
if (seps.length > 1) {
|
||||
return 'multiple';
|
||||
}
|
||||
|
||||
const sep = seps[0] ?? '\uE0B0';
|
||||
const presets = [
|
||||
{ char: '\uE0B0', name: 'Triangle Right' },
|
||||
{ char: '\uE0B2', name: 'Triangle Left' },
|
||||
{ char: '\uE0B4', name: 'Round Right' },
|
||||
{ char: '\uE0B6', name: 'Round Left' }
|
||||
];
|
||||
const preset = presets.find(item => item.char === sep);
|
||||
|
||||
if (preset) {
|
||||
return `${preset.char} - ${preset.name}`;
|
||||
}
|
||||
|
||||
return `${sep} - Custom`;
|
||||
}
|
||||
|
||||
export function getCapDisplay(
|
||||
powerlineConfig: PowerlineConfig,
|
||||
type: 'start' | 'end'
|
||||
): string {
|
||||
const caps = type === 'start'
|
||||
? powerlineConfig.startCaps
|
||||
: powerlineConfig.endCaps;
|
||||
|
||||
if (caps.length === 0) {
|
||||
return 'none';
|
||||
}
|
||||
|
||||
if (caps.length > 1) {
|
||||
return 'multiple';
|
||||
}
|
||||
|
||||
const cap = caps[0];
|
||||
|
||||
if (!cap) {
|
||||
return 'none';
|
||||
}
|
||||
|
||||
const presets = type === 'start' ? [
|
||||
{ char: '\uE0B2', name: 'Triangle' },
|
||||
{ char: '\uE0B6', name: 'Round' },
|
||||
{ char: '\uE0BA', name: 'Lower Triangle' },
|
||||
{ char: '\uE0BE', name: 'Diagonal' }
|
||||
] : [
|
||||
{ char: '\uE0B0', name: 'Triangle' },
|
||||
{ char: '\uE0B4', name: 'Round' },
|
||||
{ char: '\uE0B8', name: 'Lower Triangle' },
|
||||
{ char: '\uE0BC', name: 'Diagonal' }
|
||||
];
|
||||
const preset = presets.find(item => item.char === cap);
|
||||
|
||||
if (preset) {
|
||||
return `${preset.char} - ${preset.name}`;
|
||||
}
|
||||
|
||||
return `${cap} - Custom`;
|
||||
}
|
||||
|
||||
export function getThemeDisplay(powerlineConfig: PowerlineConfig): string {
|
||||
const theme = powerlineConfig.theme;
|
||||
|
||||
if (!theme || theme === 'custom') {
|
||||
return 'Custom';
|
||||
}
|
||||
|
||||
return theme.charAt(0).toUpperCase() + theme.slice(1);
|
||||
}
|
||||
|
||||
export function buildPowerlineSetupMenuItems(
|
||||
powerlineConfig: PowerlineConfig
|
||||
): ListEntry<PowerlineMenuValue>[] {
|
||||
const disabled = !powerlineConfig.enabled;
|
||||
|
||||
return [
|
||||
{
|
||||
label: formatPowerlineMenuLabel('Separator'),
|
||||
sublabel: `(${getSeparatorDisplay(powerlineConfig)})`,
|
||||
value: 'separator',
|
||||
disabled,
|
||||
description: 'Choose the glyph used between powerline segments.'
|
||||
},
|
||||
{
|
||||
label: formatPowerlineMenuLabel('Start Cap'),
|
||||
sublabel: `(${getCapDisplay(powerlineConfig, 'start')})`,
|
||||
value: 'startCap',
|
||||
disabled,
|
||||
description: 'Configure the cap glyph that appears at the start of each powerline line.'
|
||||
},
|
||||
{
|
||||
label: formatPowerlineMenuLabel('End Cap'),
|
||||
sublabel: `(${getCapDisplay(powerlineConfig, 'end')})`,
|
||||
value: 'endCap',
|
||||
disabled,
|
||||
description: 'Configure the cap glyph that appears at the end of each powerline line.'
|
||||
},
|
||||
{
|
||||
label: formatPowerlineMenuLabel('Themes'),
|
||||
sublabel: `(${getThemeDisplay(powerlineConfig)})`,
|
||||
value: 'themes',
|
||||
disabled,
|
||||
description: 'Preview built-in powerline themes or copy a theme into custom widget colors.'
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
export interface PowerlineSetupProps {
|
||||
settings: Settings;
|
||||
powerlineFontStatus: PowerlineFontStatus;
|
||||
@@ -25,8 +150,6 @@ export interface PowerlineSetupProps {
|
||||
onClearMessage: () => void;
|
||||
}
|
||||
|
||||
type Screen = 'menu' | 'separator' | 'startCap' | 'endCap' | 'themes';
|
||||
|
||||
export const PowerlineSetup: React.FC<PowerlineSetupProps> = ({
|
||||
settings,
|
||||
powerlineFontStatus,
|
||||
@@ -43,138 +166,55 @@ export const PowerlineSetup: React.FC<PowerlineSetupProps> = ({
|
||||
const [confirmingEnable, setConfirmingEnable] = useState(false);
|
||||
const [confirmingFontInstall, setConfirmingFontInstall] = useState(false);
|
||||
|
||||
// Check if there are any separators or flex-separators in the current configuration
|
||||
const hasSeparatorItems = settings.lines.some(line => line.some(item => item.type === 'separator' || item.type === 'flex-separator'));
|
||||
|
||||
// Menu items for navigation
|
||||
const menuItems = [
|
||||
{ label: 'Separator', value: 'separator' },
|
||||
{ label: 'Start Cap', value: 'startCap' },
|
||||
{ label: 'End Cap', value: 'endCap' },
|
||||
{ label: 'Themes', value: 'themes' },
|
||||
{ label: '← Back', value: 'back' }
|
||||
];
|
||||
|
||||
// Helper functions for display
|
||||
const getSeparatorDisplay = (): string => {
|
||||
const seps = powerlineConfig.separators;
|
||||
if (seps.length > 1) {
|
||||
return 'multiple';
|
||||
}
|
||||
const sep = seps[0] ?? '\uE0B0';
|
||||
const presets = [
|
||||
{ char: '\uE0B0', name: 'Triangle Right' },
|
||||
{ char: '\uE0B2', name: 'Triangle Left' },
|
||||
{ char: '\uE0B4', name: 'Round Right' },
|
||||
{ char: '\uE0B6', name: 'Round Left' }
|
||||
];
|
||||
const preset = presets.find(p => p.char === sep);
|
||||
if (preset) {
|
||||
return `${preset.char} - ${preset.name}`;
|
||||
}
|
||||
return `${sep} - Custom`;
|
||||
};
|
||||
|
||||
const getCapDisplay = (type: 'start' | 'end'): string => {
|
||||
const caps = type === 'start'
|
||||
? powerlineConfig.startCaps
|
||||
: powerlineConfig.endCaps;
|
||||
|
||||
if (caps.length === 0)
|
||||
return 'none';
|
||||
if (caps.length > 1)
|
||||
return 'multiple';
|
||||
|
||||
const cap = caps[0];
|
||||
if (!cap)
|
||||
return 'none';
|
||||
|
||||
const presets = type === 'start' ? [
|
||||
{ char: '\uE0B2', name: 'Triangle' },
|
||||
{ char: '\uE0B6', name: 'Round' },
|
||||
{ char: '\uE0BA', name: 'Lower Triangle' },
|
||||
{ char: '\uE0BE', name: 'Diagonal' }
|
||||
] : [
|
||||
{ char: '\uE0B0', name: 'Triangle' },
|
||||
{ char: '\uE0B4', name: 'Round' },
|
||||
{ char: '\uE0B8', name: 'Lower Triangle' },
|
||||
{ char: '\uE0BC', name: 'Diagonal' }
|
||||
];
|
||||
|
||||
const preset = presets.find(c => c.char === cap);
|
||||
if (preset) {
|
||||
return `${preset.char} - ${preset.name}`;
|
||||
}
|
||||
return `${cap} - Custom`;
|
||||
};
|
||||
|
||||
const getThemeDisplay = (): string => {
|
||||
const theme = powerlineConfig.theme;
|
||||
if (!theme || theme === 'custom')
|
||||
return 'Custom';
|
||||
return theme.charAt(0).toUpperCase() + theme.slice(1);
|
||||
};
|
||||
const hasSeparatorItems = settings.lines.some(line => line.some(
|
||||
item => item.type === 'separator' || item.type === 'flex-separator'
|
||||
));
|
||||
|
||||
useInput((input, key) => {
|
||||
// Block all input handling when font installation message is shown or installing
|
||||
if (fontInstallMessage || installingFonts) {
|
||||
// Only clear message on non-escape keys when message is shown
|
||||
if (fontInstallMessage && !key.escape) {
|
||||
onClearMessage();
|
||||
}
|
||||
// Always return early to prevent any other input handling
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip input handling when confirmations are active - let ConfirmDialog handle it
|
||||
if (confirmingFontInstall || confirmingEnable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (screen === 'menu') {
|
||||
// Menu navigation mode
|
||||
if (key.escape) {
|
||||
onBack();
|
||||
} else if (key.upArrow) {
|
||||
setSelectedMenuItem(Math.max(0, selectedMenuItem - 1));
|
||||
} else if (key.downArrow) {
|
||||
setSelectedMenuItem(Math.min(menuItems.length - 1, selectedMenuItem + 1));
|
||||
} else if (key.return) {
|
||||
const selected = menuItems[selectedMenuItem];
|
||||
if (selected) {
|
||||
if (selected.value === 'back') {
|
||||
onBack();
|
||||
} else if (powerlineConfig.enabled) {
|
||||
setScreen(selected.value as Screen);
|
||||
}
|
||||
}
|
||||
} else if (input === 't' || input === 'T') {
|
||||
// Toggle powerline mode
|
||||
if (!powerlineConfig.enabled) {
|
||||
// Only show confirmation when enabling if there are separators to remove
|
||||
if (hasSeparatorItems) {
|
||||
setConfirmingEnable(true);
|
||||
} else {
|
||||
// Enable directly without confirmation since there are no separators.
|
||||
onUpdate(buildEnabledPowerlineSettings(settings, false));
|
||||
}
|
||||
} else {
|
||||
// Disable without confirmation
|
||||
const newConfig = { ...powerlineConfig, enabled: false };
|
||||
onUpdate({ ...settings, powerline: newConfig });
|
||||
onUpdate({
|
||||
...settings,
|
||||
powerline: {
|
||||
...powerlineConfig,
|
||||
enabled: false
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (input === 'i' || input === 'I') {
|
||||
// Show font installation consent prompt
|
||||
setConfirmingFontInstall(true);
|
||||
} else if ((input === 'a' || input === 'A') && powerlineConfig.enabled) {
|
||||
// Toggle autoAlign when powerline is enabled
|
||||
const newConfig = { ...powerlineConfig, autoAlign: !powerlineConfig.autoAlign };
|
||||
onUpdate({ ...settings, powerline: newConfig });
|
||||
onUpdate({
|
||||
...settings,
|
||||
powerline: {
|
||||
...powerlineConfig,
|
||||
autoAlign: !powerlineConfig.autoAlign
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Render sub-screens
|
||||
if (screen === 'separator') {
|
||||
return (
|
||||
<PowerlineSeparatorEditor
|
||||
@@ -218,7 +258,6 @@ export const PowerlineSetup: React.FC<PowerlineSetupProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
// Main menu screen
|
||||
return (
|
||||
<Box flexDirection='column'>
|
||||
{!confirmingFontInstall && !installingFonts && !fontInstallMessage && (
|
||||
@@ -374,62 +413,29 @@ export const PowerlineSetup: React.FC<PowerlineSetupProps> = ({
|
||||
</>
|
||||
)}
|
||||
|
||||
<Box marginTop={1} flexDirection='column'>
|
||||
{powerlineConfig.enabled ? (
|
||||
<>
|
||||
{menuItems.map((item, index) => {
|
||||
const isSelected = index === selectedMenuItem;
|
||||
let displayValue = '';
|
||||
{!powerlineConfig.enabled && (
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>Enable Powerline mode to configure separators, caps, and themes.</Text>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
switch (item.value) {
|
||||
case 'separator':
|
||||
displayValue = getSeparatorDisplay();
|
||||
break;
|
||||
case 'startCap':
|
||||
displayValue = getCapDisplay('start');
|
||||
break;
|
||||
case 'endCap':
|
||||
displayValue = getCapDisplay('end');
|
||||
break;
|
||||
case 'themes':
|
||||
displayValue = getThemeDisplay();
|
||||
break;
|
||||
case 'back':
|
||||
displayValue = '';
|
||||
break;
|
||||
}
|
||||
<List
|
||||
marginTop={1}
|
||||
items={buildPowerlineSetupMenuItems(powerlineConfig)}
|
||||
onSelect={(value) => {
|
||||
if (value === 'back') {
|
||||
onBack();
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.value === 'back') {
|
||||
return (
|
||||
<Box key={item.value} marginTop={1}>
|
||||
<Text color={isSelected ? 'green' : undefined}>
|
||||
{isSelected ? '▶ ' : ' '}
|
||||
{item.label}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box key={item.value}>
|
||||
<Text color={isSelected ? 'green' : undefined}>
|
||||
{isSelected ? '▶ ' : ' '}
|
||||
{item.label.padEnd(11, ' ')}
|
||||
<Text dimColor>
|
||||
{displayValue && `(${displayValue})`}
|
||||
</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
// When powerline is disabled, show ESC to go back message
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>Press ESC to go back</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
setScreen(value);
|
||||
}}
|
||||
onSelectionChange={(_, index) => {
|
||||
setSelectedMenuItem(index);
|
||||
}}
|
||||
initialSelection={selectedMenuItem}
|
||||
showBackButton={true}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -4,6 +4,8 @@ import {
|
||||
useInput
|
||||
} from 'ink';
|
||||
import React, {
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState
|
||||
} from 'react';
|
||||
@@ -16,6 +18,74 @@ import {
|
||||
} from '../../utils/colors';
|
||||
|
||||
import { ConfirmDialog } from './ConfirmDialog';
|
||||
import {
|
||||
List,
|
||||
type ListEntry
|
||||
} from './List';
|
||||
|
||||
export function buildPowerlineThemeItems(
|
||||
themes: string[],
|
||||
originalTheme: string
|
||||
): ListEntry<string>[] {
|
||||
return themes.map((themeName) => {
|
||||
const theme = getPowerlineTheme(themeName);
|
||||
|
||||
return {
|
||||
label: theme?.name ?? themeName,
|
||||
sublabel: themeName === originalTheme ? '(original)' : undefined,
|
||||
value: themeName,
|
||||
description: theme?.description ?? ''
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function applyCustomPowerlineTheme(
|
||||
settings: Settings,
|
||||
themeName: string
|
||||
): Settings | null {
|
||||
const theme = getPowerlineTheme(themeName);
|
||||
|
||||
if (!theme || themeName === 'custom') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const colorLevel = getColorLevelString(settings.colorLevel);
|
||||
const colorLevelKey = colorLevel === 'ansi16' ? '1' : colorLevel === 'ansi256' ? '2' : '3';
|
||||
const themeColors = theme[colorLevelKey];
|
||||
|
||||
if (!themeColors) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const lines = settings.lines.map((line) => {
|
||||
let widgetColorIndex = 0;
|
||||
|
||||
return line.map((widget) => {
|
||||
if (widget.type === 'separator' || widget.type === 'flex-separator') {
|
||||
return widget;
|
||||
}
|
||||
|
||||
const fgColor = themeColors.fg[widgetColorIndex % themeColors.fg.length];
|
||||
const bgColor = themeColors.bg[widgetColorIndex % themeColors.bg.length];
|
||||
widgetColorIndex++;
|
||||
|
||||
return {
|
||||
...widget,
|
||||
color: fgColor,
|
||||
backgroundColor: bgColor
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
...settings,
|
||||
powerline: {
|
||||
...settings.powerline,
|
||||
theme: 'custom'
|
||||
},
|
||||
lines
|
||||
};
|
||||
}
|
||||
|
||||
export interface PowerlineThemeSelectorProps {
|
||||
settings: Settings;
|
||||
@@ -28,120 +98,63 @@ export const PowerlineThemeSelector: React.FC<PowerlineThemeSelectorProps> = ({
|
||||
onUpdate,
|
||||
onBack
|
||||
}) => {
|
||||
const themes = getPowerlineThemes();
|
||||
const themes = useMemo(() => getPowerlineThemes(), []);
|
||||
const currentTheme = settings.powerline.theme ?? 'custom';
|
||||
const [selectedIndex, setSelectedIndex] = useState(Math.max(0, themes.indexOf(currentTheme)));
|
||||
const [showCustomizeConfirm, setShowCustomizeConfirm] = useState(false);
|
||||
const originalThemeRef = useRef(currentTheme);
|
||||
const originalSettingsRef = useRef(settings);
|
||||
const latestSettingsRef = useRef(settings);
|
||||
const latestOnUpdateRef = useRef(onUpdate);
|
||||
const didHandleInitialSelectionRef = useRef(false);
|
||||
|
||||
const applyTheme = (themeName: string) => {
|
||||
// Simply change the theme setting, don't modify widget colors
|
||||
const updatedSettings = {
|
||||
...settings,
|
||||
useEffect(() => {
|
||||
latestSettingsRef.current = settings;
|
||||
latestOnUpdateRef.current = onUpdate;
|
||||
}, [settings, onUpdate]);
|
||||
|
||||
useEffect(() => {
|
||||
const themeName = themes[selectedIndex];
|
||||
|
||||
if (!themeName) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!didHandleInitialSelectionRef.current) {
|
||||
didHandleInitialSelectionRef.current = true;
|
||||
return;
|
||||
}
|
||||
|
||||
latestOnUpdateRef.current({
|
||||
...latestSettingsRef.current,
|
||||
powerline: {
|
||||
...settings.powerline,
|
||||
...latestSettingsRef.current.powerline,
|
||||
theme: themeName
|
||||
}
|
||||
};
|
||||
onUpdate(updatedSettings);
|
||||
};
|
||||
|
||||
const customizeTheme = () => {
|
||||
// Copy current theme's colors to widgets and switch to custom theme
|
||||
const currentThemeName = themes[selectedIndex];
|
||||
if (!currentThemeName) {
|
||||
return;
|
||||
}
|
||||
const theme = getPowerlineTheme(currentThemeName);
|
||||
|
||||
if (!theme || currentThemeName === 'custom') {
|
||||
// If already on custom, just go back
|
||||
onBack();
|
||||
return;
|
||||
}
|
||||
|
||||
const colorLevel = getColorLevelString(settings.colorLevel);
|
||||
const colorLevelKey = colorLevel === 'ansi16' ? '1' : colorLevel === 'ansi256' ? '2' : '3';
|
||||
const themeColors = theme[colorLevelKey];
|
||||
|
||||
if (themeColors) {
|
||||
// Apply theme colors to widgets
|
||||
const newLines = settings.lines.map((line) => {
|
||||
let widgetColorIndex = 0;
|
||||
return line.map((widget) => {
|
||||
// Skip separators
|
||||
if (widget.type === 'separator' || widget.type === 'flex-separator') {
|
||||
return widget;
|
||||
}
|
||||
|
||||
const fgColor = themeColors.fg[widgetColorIndex % themeColors.fg.length];
|
||||
const bgColor = themeColors.bg[widgetColorIndex % themeColors.bg.length];
|
||||
widgetColorIndex++;
|
||||
|
||||
return {
|
||||
...widget,
|
||||
color: fgColor,
|
||||
backgroundColor: bgColor
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const updatedSettings = {
|
||||
...settings,
|
||||
powerline: {
|
||||
...settings.powerline,
|
||||
theme: 'custom'
|
||||
},
|
||||
lines: newLines
|
||||
};
|
||||
|
||||
onUpdate(updatedSettings);
|
||||
}
|
||||
|
||||
onBack();
|
||||
};
|
||||
});
|
||||
}, [selectedIndex, themes]);
|
||||
|
||||
useInput((input, key) => {
|
||||
// Skip input handling when confirmation is active - let ConfirmDialog handle it
|
||||
if (showCustomizeConfirm) {
|
||||
return;
|
||||
}
|
||||
{
|
||||
// Normal input handling
|
||||
if (key.escape) {
|
||||
// Restore original settings completely when canceling
|
||||
onUpdate(originalSettingsRef.current);
|
||||
onBack();
|
||||
} else if (key.upArrow) {
|
||||
const newIndex = Math.max(0, selectedIndex - 1);
|
||||
setSelectedIndex(newIndex);
|
||||
const newTheme = themes[newIndex];
|
||||
if (newTheme) {
|
||||
applyTheme(newTheme);
|
||||
}
|
||||
} else if (key.downArrow) {
|
||||
const newIndex = Math.min(themes.length - 1, selectedIndex + 1);
|
||||
setSelectedIndex(newIndex);
|
||||
const newTheme = themes[newIndex];
|
||||
if (newTheme) {
|
||||
applyTheme(newTheme);
|
||||
}
|
||||
} else if (key.return) {
|
||||
// User confirmed their selection, so we keep the current theme
|
||||
onBack();
|
||||
} else if (input === 'c' || input === 'C') {
|
||||
// Customize theme - copy theme colors to widgets
|
||||
const currentThemeName = themes[selectedIndex];
|
||||
if (currentThemeName && currentThemeName !== 'custom') {
|
||||
setShowCustomizeConfirm(true);
|
||||
}
|
||||
|
||||
if (key.escape) {
|
||||
onUpdate(originalSettingsRef.current);
|
||||
onBack();
|
||||
} else if (input === 'c' || input === 'C') {
|
||||
const currentThemeName = themes[selectedIndex];
|
||||
if (currentThemeName && currentThemeName !== 'custom') {
|
||||
setShowCustomizeConfirm(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const selectedThemeName = themes[selectedIndex];
|
||||
const selectedTheme = selectedThemeName ? getPowerlineTheme(selectedThemeName) : undefined;
|
||||
const themeItems = useMemo(
|
||||
() => buildPowerlineThemeItems(themes, originalThemeRef.current),
|
||||
[themes]
|
||||
);
|
||||
|
||||
if (showCustomizeConfirm) {
|
||||
return (
|
||||
@@ -159,8 +172,14 @@ export const PowerlineThemeSelector: React.FC<PowerlineThemeSelectorProps> = ({
|
||||
<ConfirmDialog
|
||||
inline={true}
|
||||
onConfirm={() => {
|
||||
customizeTheme();
|
||||
if (selectedThemeName) {
|
||||
const updatedSettings = applyCustomPowerlineTheme(settings, selectedThemeName);
|
||||
if (updatedSettings) {
|
||||
onUpdate(updatedSettings);
|
||||
}
|
||||
}
|
||||
setShowCustomizeConfirm(false);
|
||||
onBack();
|
||||
}}
|
||||
onCancel={() => {
|
||||
setShowCustomizeConfirm(false);
|
||||
@@ -185,40 +204,30 @@ export const PowerlineThemeSelector: React.FC<PowerlineThemeSelectorProps> = ({
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1} flexDirection='column'>
|
||||
{themes.map((themeName, index) => {
|
||||
const theme = getPowerlineTheme(themeName);
|
||||
const isSelected = index === selectedIndex;
|
||||
const isOriginal = themeName === originalThemeRef.current;
|
||||
<List
|
||||
marginTop={1}
|
||||
items={themeItems}
|
||||
onSelect={() => {
|
||||
onBack();
|
||||
}}
|
||||
onSelectionChange={(themeName, index) => {
|
||||
if (themeName === 'back') {
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box key={themeName}>
|
||||
<Text color={isSelected ? 'green' : undefined}>
|
||||
{isSelected ? '▶ ' : ' '}
|
||||
{theme?.name ?? themeName}
|
||||
{isOriginal && <Text dimColor> (original)</Text>}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
setSelectedIndex(index);
|
||||
}}
|
||||
initialSelection={selectedIndex}
|
||||
/>
|
||||
|
||||
{selectedTheme && (
|
||||
<Box marginTop={2} flexDirection='column'>
|
||||
<Text dimColor>Description:</Text>
|
||||
<Box marginLeft={2}>
|
||||
<Text>{selectedTheme.description}</Text>
|
||||
</Box>
|
||||
{selectedThemeName && selectedThemeName !== 'custom' && (
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>Press (c) to customize this theme - copies colors to widgets</Text>
|
||||
</Box>
|
||||
)}
|
||||
{settings.colorLevel === 1 && (
|
||||
<Box>
|
||||
<Text color='yellow'>⚠ 16 color mode themes have a very limited palette, we recommend switching color level in Terminal Options</Text>
|
||||
</Box>
|
||||
)}
|
||||
{selectedThemeName && selectedThemeName !== 'custom' && (
|
||||
<Box marginTop={1}>
|
||||
<Text dimColor>Press (c) to customize this theme - copies colors to widgets</Text>
|
||||
</Box>
|
||||
)}
|
||||
{settings.colorLevel === 1 && (
|
||||
<Box marginTop={1}>
|
||||
<Text color='yellow'>⚠ 16 color mode themes have a very limited palette, we recommend switching color level in Terminal Options</Text>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -13,6 +13,50 @@ import {
|
||||
} from '../../utils/color-sanitize';
|
||||
|
||||
import { ConfirmDialog } from './ConfirmDialog';
|
||||
import {
|
||||
List,
|
||||
type ListEntry
|
||||
} from './List';
|
||||
|
||||
type TerminalOptionsValue = 'width' | 'colorLevel';
|
||||
|
||||
export function getNextColorLevel(level: 0 | 1 | 2 | 3): 0 | 1 | 2 | 3 {
|
||||
return ((level + 1) % 4) as 0 | 1 | 2 | 3;
|
||||
}
|
||||
|
||||
export function shouldWarnOnColorLevelChange(
|
||||
currentLevel: 0 | 1 | 2 | 3,
|
||||
nextLevel: 0 | 1 | 2 | 3,
|
||||
hasCustomColors: boolean
|
||||
): boolean {
|
||||
return hasCustomColors
|
||||
&& ((currentLevel === 2 && nextLevel !== 2)
|
||||
|| (currentLevel === 3 && nextLevel !== 3));
|
||||
}
|
||||
|
||||
export function buildTerminalOptionsItems(
|
||||
colorLevel: 0 | 1 | 2 | 3
|
||||
): ListEntry<TerminalOptionsValue>[] {
|
||||
return [
|
||||
{
|
||||
label: '◱ Terminal Width',
|
||||
value: 'width',
|
||||
description: 'Configure how the status line uses available terminal width and when it should compact.'
|
||||
},
|
||||
{
|
||||
label: '▓ Color Level',
|
||||
sublabel: `(${getColorLevelLabel(colorLevel)})`,
|
||||
value: 'colorLevel',
|
||||
description: [
|
||||
'Color level affects how colors are rendered:',
|
||||
'• Truecolor: Full 24-bit RGB colors (16.7M colors)',
|
||||
'• 256 Color: Extended color palette (256 colors)',
|
||||
'• Basic: Standard 16-color terminal palette',
|
||||
'• No Color: Disables all color output'
|
||||
].join('\n')
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
export interface TerminalOptionsMenuProps {
|
||||
settings: Settings;
|
||||
@@ -20,49 +64,47 @@ export interface TerminalOptionsMenuProps {
|
||||
onBack: (target?: string) => void;
|
||||
}
|
||||
|
||||
export const TerminalOptionsMenu: React.FC<TerminalOptionsMenuProps> = ({ settings, onUpdate, onBack }) => {
|
||||
export const TerminalOptionsMenu: React.FC<TerminalOptionsMenuProps> = ({
|
||||
settings,
|
||||
onUpdate,
|
||||
onBack
|
||||
}) => {
|
||||
const [showColorWarning, setShowColorWarning] = useState(false);
|
||||
const [pendingColorLevel, setPendingColorLevel] = useState<0 | 1 | 2 | 3 | null>(null);
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
|
||||
const handleSelect = () => {
|
||||
if (selectedIndex === 2) {
|
||||
// Back button
|
||||
const handleSelect = (value: TerminalOptionsValue | 'back') => {
|
||||
if (value === 'back') {
|
||||
onBack();
|
||||
} else if (selectedIndex === 0) {
|
||||
// Terminal Width Options
|
||||
onBack('width');
|
||||
} else if (selectedIndex === 1) {
|
||||
// Color Level
|
||||
// Check if there are any custom colors that would be lost
|
||||
const hasCustomColors = hasCustomWidgetColors(settings.lines);
|
||||
|
||||
const currentLevel = settings.colorLevel;
|
||||
const nextLevel = ((currentLevel + 1) % 4) as 0 | 1 | 2 | 3;
|
||||
|
||||
// Warn if switching away from mode that supports custom colors
|
||||
if (hasCustomColors
|
||||
&& ((currentLevel === 2 && nextLevel !== 2) // Switching from 256 color mode
|
||||
|| (currentLevel === 3 && nextLevel !== 3))) { // Switching from truecolor mode
|
||||
setShowColorWarning(true);
|
||||
setPendingColorLevel(nextLevel);
|
||||
} else {
|
||||
// Update chalk level immediately
|
||||
chalk.level = nextLevel;
|
||||
|
||||
const cleanedLines = sanitizeLinesForColorLevel(settings.lines, nextLevel);
|
||||
|
||||
onUpdate({
|
||||
...settings,
|
||||
lines: cleanedLines,
|
||||
colorLevel: nextLevel
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (value === 'width') {
|
||||
onBack('width');
|
||||
return;
|
||||
}
|
||||
|
||||
const hasCustomColors = hasCustomWidgetColors(settings.lines);
|
||||
const currentLevel = settings.colorLevel;
|
||||
const nextLevel = getNextColorLevel(currentLevel);
|
||||
|
||||
if (shouldWarnOnColorLevelChange(currentLevel, nextLevel, hasCustomColors)) {
|
||||
setShowColorWarning(true);
|
||||
setPendingColorLevel(nextLevel);
|
||||
return;
|
||||
}
|
||||
|
||||
chalk.level = nextLevel;
|
||||
|
||||
const cleanedLines = sanitizeLinesForColorLevel(settings.lines, nextLevel);
|
||||
|
||||
onUpdate({
|
||||
...settings,
|
||||
lines: cleanedLines,
|
||||
colorLevel: nextLevel
|
||||
});
|
||||
};
|
||||
|
||||
const handleColorConfirm = () => {
|
||||
// Proceed with color level change and clean up custom colors
|
||||
if (pendingColorLevel !== null) {
|
||||
chalk.level = pendingColorLevel;
|
||||
|
||||
@@ -83,19 +125,9 @@ export const TerminalOptionsMenu: React.FC<TerminalOptionsMenuProps> = ({ settin
|
||||
setPendingColorLevel(null);
|
||||
};
|
||||
|
||||
useInput((input, key) => {
|
||||
if (key.escape) {
|
||||
if (!showColorWarning) {
|
||||
onBack();
|
||||
}
|
||||
} else if (!showColorWarning) {
|
||||
if (key.upArrow) {
|
||||
setSelectedIndex(Math.max(0, selectedIndex - 1));
|
||||
} else if (key.downArrow) {
|
||||
setSelectedIndex(Math.min(2, selectedIndex + 1));
|
||||
} else if (key.return) {
|
||||
handleSelect();
|
||||
}
|
||||
useInput((_, key) => {
|
||||
if (key.escape && !showColorWarning) {
|
||||
onBack();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -118,39 +150,12 @@ export const TerminalOptionsMenu: React.FC<TerminalOptionsMenuProps> = ({ settin
|
||||
) : (
|
||||
<>
|
||||
<Text color='white'>Configure terminal-specific settings for optimal display</Text>
|
||||
<Box marginTop={1} flexDirection='column'>
|
||||
<Box>
|
||||
<Text color={selectedIndex === 0 ? 'green' : undefined}>
|
||||
{selectedIndex === 0 ? '▶ ' : ' '}
|
||||
◱ Terminal Width
|
||||
</Text>
|
||||
</Box>
|
||||
<Box>
|
||||
<Text color={selectedIndex === 1 ? 'green' : undefined}>
|
||||
{selectedIndex === 1 ? '▶ ' : ' '}
|
||||
▓ Color Level:
|
||||
{' '}
|
||||
{getColorLevelLabel(settings.colorLevel)}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text color={selectedIndex === 2 ? 'green' : undefined}>
|
||||
{selectedIndex === 2 ? '▶ ' : ' '}
|
||||
← Back
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{selectedIndex === 1 && (
|
||||
<Box marginTop={1} flexDirection='column'>
|
||||
<Text dimColor>Color level affects how colors are rendered:</Text>
|
||||
<Text dimColor>• Truecolor: Full 24-bit RGB colors (16.7M colors)</Text>
|
||||
<Text dimColor>• 256 Color: Extended color palette (256 colors)</Text>
|
||||
<Text dimColor>• Basic: Standard 16-color terminal palette</Text>
|
||||
<Text dimColor>• No Color: Disables all color output</Text>
|
||||
</Box>
|
||||
)}
|
||||
<List
|
||||
marginTop={1}
|
||||
items={buildTerminalOptionsItems(settings.colorLevel)}
|
||||
onSelect={handleSelect}
|
||||
showBackButton={true}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -9,38 +9,89 @@ import type { FlexMode } from '../../types/FlexMode';
|
||||
import type { Settings } from '../../types/Settings';
|
||||
import { shouldInsertInput } from '../../utils/input-guards';
|
||||
|
||||
import {
|
||||
List,
|
||||
type ListEntry
|
||||
} from './List';
|
||||
|
||||
export const TERMINAL_WIDTH_OPTIONS: FlexMode[] = ['full', 'full-minus-40', 'full-until-compact'];
|
||||
|
||||
export function getTerminalWidthSelectionIndex(selectedOption: FlexMode): number {
|
||||
const selectedIndex = TERMINAL_WIDTH_OPTIONS.indexOf(selectedOption);
|
||||
|
||||
return selectedIndex >= 0 ? selectedIndex : 0;
|
||||
}
|
||||
|
||||
export function validateCompactThresholdInput(value: string): string | null {
|
||||
const parsedValue = parseInt(value, 10);
|
||||
|
||||
if (isNaN(parsedValue)) {
|
||||
return 'Please enter a valid number';
|
||||
}
|
||||
|
||||
if (parsedValue < 1 || parsedValue > 99) {
|
||||
return `Value must be between 1 and 99 (you entered ${parsedValue})`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function buildTerminalWidthItems(
|
||||
selectedOption: FlexMode,
|
||||
compactThreshold: number
|
||||
): ListEntry<FlexMode>[] {
|
||||
return [
|
||||
{
|
||||
value: 'full',
|
||||
label: 'Full width always',
|
||||
sublabel: selectedOption === 'full' ? '(active)' : undefined,
|
||||
description: 'Uses the full terminal width minus 4 characters for terminal padding. If the auto-compact message appears, it may cause the line to wrap.\n\nNOTE: If /ide integration is enabled, it is not recommended to use this mode.'
|
||||
},
|
||||
{
|
||||
value: 'full-minus-40',
|
||||
label: 'Full width minus 40',
|
||||
sublabel: selectedOption === 'full-minus-40' ? '(active)' : '(default)',
|
||||
description: 'Leaves a gap to the right of the status line to accommodate the auto-compact message. This prevents wrapping but may leave unused space. This limitation exists because we cannot detect when the message will appear.'
|
||||
},
|
||||
{
|
||||
value: 'full-until-compact',
|
||||
label: 'Full width until compact',
|
||||
sublabel: selectedOption === 'full-until-compact'
|
||||
? `(threshold ${compactThreshold}%, active)`
|
||||
: `(threshold ${compactThreshold}%)`,
|
||||
description: `Dynamically adjusts width based on context usage. When context reaches ${compactThreshold}%, it switches to leaving space for the auto-compact message.\n\nNOTE: If /ide integration is enabled, it is not recommended to use this mode.`
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
export interface TerminalWidthMenuProps {
|
||||
settings: Settings;
|
||||
onUpdate: (settings: Settings) => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
export const TerminalWidthMenu: React.FC<TerminalWidthMenuProps> = ({ settings, onUpdate, onBack }) => {
|
||||
export const TerminalWidthMenu: React.FC<TerminalWidthMenuProps> = ({
|
||||
settings,
|
||||
onUpdate,
|
||||
onBack
|
||||
}) => {
|
||||
const [selectedOption, setSelectedOption] = useState<FlexMode>(settings.flexMode);
|
||||
const [compactThreshold, setCompactThreshold] = useState(settings.compactThreshold);
|
||||
const [editingThreshold, setEditingThreshold] = useState(false);
|
||||
const [thresholdInput, setThresholdInput] = useState(String(settings.compactThreshold));
|
||||
const [validationError, setValidationError] = useState<string | null>(null);
|
||||
|
||||
// For manual navigation: 0-2 for options, 3 for back
|
||||
const [selectedIndex, setSelectedIndex] = useState(() => {
|
||||
const options: FlexMode[] = ['full', 'full-minus-40', 'full-until-compact'];
|
||||
return options.indexOf(settings.flexMode);
|
||||
});
|
||||
|
||||
const options: FlexMode[] = ['full', 'full-minus-40', 'full-until-compact'];
|
||||
|
||||
useInput((input, key) => {
|
||||
if (editingThreshold) {
|
||||
if (key.return) {
|
||||
const value = parseInt(thresholdInput, 10);
|
||||
if (isNaN(value)) {
|
||||
setValidationError('Please enter a valid number');
|
||||
} else if (value < 1 || value > 99) {
|
||||
setValidationError(`Value must be between 1 and 99 (you entered ${value})`);
|
||||
const error = validateCompactThresholdInput(thresholdInput);
|
||||
|
||||
if (error) {
|
||||
setValidationError(error);
|
||||
} else {
|
||||
const value = parseInt(thresholdInput, 10);
|
||||
setCompactThreshold(value);
|
||||
// Update settings with both flexMode and the new threshold
|
||||
|
||||
const updatedSettings = {
|
||||
...settings,
|
||||
flexMode: selectedOption,
|
||||
@@ -66,59 +117,14 @@ export const TerminalWidthMenu: React.FC<TerminalWidthMenuProps> = ({ settings,
|
||||
setValidationError(null);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (key.escape) {
|
||||
onBack();
|
||||
} else if (key.upArrow) {
|
||||
setSelectedIndex(Math.max(0, selectedIndex - 1));
|
||||
} else if (key.downArrow) {
|
||||
setSelectedIndex(Math.min(3, selectedIndex + 1)); // 0-2 for options, 3 for back
|
||||
} else if (key.return) {
|
||||
if (selectedIndex === 3) {
|
||||
onBack();
|
||||
} else if (selectedIndex >= 0 && selectedIndex < options.length) {
|
||||
const mode = options[selectedIndex];
|
||||
if (mode) {
|
||||
setSelectedOption(mode);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update settings
|
||||
const updatedSettings = {
|
||||
...settings,
|
||||
flexMode: mode,
|
||||
compactThreshold: compactThreshold
|
||||
};
|
||||
onUpdate(updatedSettings);
|
||||
|
||||
if (mode === 'full-until-compact') {
|
||||
// Prompt for threshold editing
|
||||
setEditingThreshold(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (key.escape) {
|
||||
onBack();
|
||||
}
|
||||
});
|
||||
|
||||
const optionDetails = [
|
||||
{
|
||||
value: 'full' as FlexMode,
|
||||
label: 'Full width always',
|
||||
description: 'Uses the full terminal width minus 4 characters for terminal padding. If the auto-compact message appears, it may cause the line to wrap.\n\nNOTE: If /ide integration is enabled, it\'s not recommended to use this mode.'
|
||||
},
|
||||
{
|
||||
value: 'full-minus-40' as FlexMode,
|
||||
label: 'Full width minus 40 (default)',
|
||||
description: 'Leaves a gap to the right of the status line to accommodate the auto-compact message. This prevents wrapping but may leave unused space. This limitation exists because we cannot detect when the message will appear.'
|
||||
},
|
||||
{
|
||||
value: 'full-until-compact' as FlexMode,
|
||||
label: 'Full width until compact',
|
||||
description: `Dynamically adjusts width based on context usage. When context reaches ${compactThreshold}%, it switches to leaving space for the auto-compact message.\n\nNOTE: If /ide integration is enabled, it's not recommended to use this mode.`
|
||||
}
|
||||
];
|
||||
|
||||
const currentOption = selectedIndex < 3 ? optionDetails[selectedIndex] : null;
|
||||
|
||||
return (
|
||||
<Box flexDirection='column'>
|
||||
<Text bold>Terminal Width</Text>
|
||||
@@ -140,38 +146,31 @@ export const TerminalWidthMenu: React.FC<TerminalWidthMenuProps> = ({ settings,
|
||||
)}
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
<Box marginTop={1} flexDirection='column'>
|
||||
{optionDetails.map((opt, index) => (
|
||||
<Box key={opt.value}>
|
||||
<Text color={selectedIndex === index ? 'green' : undefined}>
|
||||
{selectedIndex === index ? '▶ ' : ' '}
|
||||
{opt.label}
|
||||
{opt.value === selectedOption ? ' ✓' : ''}
|
||||
</Text>
|
||||
</Box>
|
||||
))}
|
||||
<List
|
||||
marginTop={1}
|
||||
items={buildTerminalWidthItems(selectedOption, compactThreshold)}
|
||||
initialSelection={getTerminalWidthSelectionIndex(selectedOption)}
|
||||
onSelect={(value) => {
|
||||
if (value === 'back') {
|
||||
onBack();
|
||||
return;
|
||||
}
|
||||
|
||||
<Box marginTop={1}>
|
||||
<Text color={selectedIndex === 3 ? 'green' : undefined}>
|
||||
{selectedIndex === 3 ? '▶ ' : ' '}
|
||||
← Back
|
||||
</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
setSelectedOption(value);
|
||||
|
||||
{currentOption && (
|
||||
<Box marginTop={1} marginBottom={1} borderStyle='round' borderColor='dim' paddingX={1}>
|
||||
<Box flexDirection='column'>
|
||||
<Text>
|
||||
<Text color='yellow'>{currentOption.label}</Text>
|
||||
{currentOption.value === 'full-until-compact' && ` | Current threshold: ${compactThreshold}%`}
|
||||
</Text>
|
||||
<Text dimColor wrap='wrap'>{currentOption.description}</Text>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</>
|
||||
const updatedSettings = {
|
||||
...settings,
|
||||
flexMode: value,
|
||||
compactThreshold
|
||||
};
|
||||
onUpdate(updatedSettings);
|
||||
|
||||
if (value === 'full-until-compact') {
|
||||
setEditingThreshold(true);
|
||||
}
|
||||
}}
|
||||
showBackButton={true}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import { render } from 'ink';
|
||||
import { PassThrough } from 'node:stream';
|
||||
import React from 'react';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
import {
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi
|
||||
} from 'vitest';
|
||||
|
||||
import { InstallMenu } from '../InstallMenu';
|
||||
|
||||
class MockTtyStream extends PassThrough {
|
||||
isTTY = true;
|
||||
columns = 120;
|
||||
rows = 40;
|
||||
|
||||
setRawMode() {
|
||||
return this;
|
||||
}
|
||||
|
||||
ref() {
|
||||
return this;
|
||||
}
|
||||
|
||||
unref() {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
interface CapturedWriteStream extends NodeJS.WriteStream { getOutput: () => string }
|
||||
|
||||
function createMockStdin(): NodeJS.ReadStream {
|
||||
return new MockTtyStream() as unknown as NodeJS.ReadStream;
|
||||
}
|
||||
|
||||
function createMockStdout(): CapturedWriteStream {
|
||||
const stream = new MockTtyStream();
|
||||
const chunks: string[] = [];
|
||||
|
||||
stream.on('data', (chunk: Buffer | string) => {
|
||||
chunks.push(chunk.toString());
|
||||
});
|
||||
|
||||
return Object.assign(stream as unknown as NodeJS.WriteStream, {
|
||||
getOutput() {
|
||||
return stripAnsi(chunks.join(''));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function flushInk() {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, 25);
|
||||
});
|
||||
}
|
||||
|
||||
describe('InstallMenu', () => {
|
||||
it('calls onCancel when escape is pressed', async () => {
|
||||
const stdin = createMockStdin();
|
||||
const stdout = createMockStdout();
|
||||
const stderr = createMockStdout();
|
||||
const onCancel = vi.fn();
|
||||
const instance = render(
|
||||
React.createElement(InstallMenu, {
|
||||
bunxAvailable: true,
|
||||
existingStatusLine: null,
|
||||
onSelectNpx: vi.fn(),
|
||||
onSelectBunx: vi.fn(),
|
||||
onCancel
|
||||
}),
|
||||
{
|
||||
stdin,
|
||||
stdout,
|
||||
stderr,
|
||||
debug: true,
|
||||
exitOnCtrlC: false,
|
||||
patchConsole: false
|
||||
}
|
||||
);
|
||||
|
||||
try {
|
||||
await flushInk();
|
||||
|
||||
stdin.write('\u001B');
|
||||
await flushInk();
|
||||
|
||||
expect(onCancel).toHaveBeenCalledTimes(1);
|
||||
} finally {
|
||||
instance.unmount();
|
||||
instance.cleanup();
|
||||
stdin.destroy();
|
||||
stdout.destroy();
|
||||
stderr.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
it('respects the provided initial selection', async () => {
|
||||
const stdin = createMockStdin();
|
||||
const stdout = createMockStdout();
|
||||
const stderr = createMockStdout();
|
||||
const instance = render(
|
||||
React.createElement(InstallMenu, {
|
||||
bunxAvailable: true,
|
||||
existingStatusLine: null,
|
||||
onSelectNpx: vi.fn(),
|
||||
onSelectBunx: vi.fn(),
|
||||
onCancel: vi.fn(),
|
||||
initialSelection: 1
|
||||
}),
|
||||
{
|
||||
stdin,
|
||||
stdout,
|
||||
stderr,
|
||||
debug: true,
|
||||
exitOnCtrlC: false,
|
||||
patchConsole: false
|
||||
}
|
||||
);
|
||||
|
||||
try {
|
||||
await flushInk();
|
||||
|
||||
expect(stdout.getOutput()).toContain('▶ bunx - Bun Package Execute');
|
||||
expect(stdout.getOutput()).not.toContain('▶ npx - Node Package Execute');
|
||||
} finally {
|
||||
instance.unmount();
|
||||
instance.cleanup();
|
||||
stdin.destroy();
|
||||
stdout.destroy();
|
||||
stderr.destroy();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import {
|
||||
describe,
|
||||
expect,
|
||||
it
|
||||
} from 'vitest';
|
||||
|
||||
import { DEFAULT_SETTINGS } from '../../../types/Settings';
|
||||
import {
|
||||
buildPowerlineSetupMenuItems,
|
||||
getCapDisplay,
|
||||
getSeparatorDisplay,
|
||||
getThemeDisplay
|
||||
} from '../PowerlineSetup';
|
||||
|
||||
describe('PowerlineSetup helpers', () => {
|
||||
it('formats separator, cap, and theme display values', () => {
|
||||
const config = {
|
||||
...DEFAULT_SETTINGS.powerline,
|
||||
enabled: true,
|
||||
separators: ['\uE0B4'],
|
||||
startCaps: ['\uE0B2'],
|
||||
endCaps: ['\uE0B0'],
|
||||
theme: 'gruvbox'
|
||||
};
|
||||
|
||||
expect(getSeparatorDisplay(config)).toBe('\uE0B4 - Round Right');
|
||||
expect(getCapDisplay(config, 'start')).toBe('\uE0B2 - Triangle');
|
||||
expect(getCapDisplay(config, 'end')).toBe('\uE0B0 - Triangle');
|
||||
expect(getThemeDisplay(config)).toBe('Gruvbox');
|
||||
});
|
||||
|
||||
it('builds powerline setup items with disabled states and sublabels', () => {
|
||||
const disabledItems = buildPowerlineSetupMenuItems({
|
||||
...DEFAULT_SETTINGS.powerline,
|
||||
enabled: false
|
||||
});
|
||||
|
||||
expect(disabledItems.every(item => item.disabled)).toBe(true);
|
||||
|
||||
const enabledItems = buildPowerlineSetupMenuItems({
|
||||
...DEFAULT_SETTINGS.powerline,
|
||||
enabled: true,
|
||||
separators: ['\uE0B0', '\uE0B4'],
|
||||
startCaps: [],
|
||||
endCaps: ['\uE0BC'],
|
||||
theme: undefined
|
||||
});
|
||||
|
||||
expect(enabledItems[0]).toMatchObject({
|
||||
label: 'Separator ',
|
||||
sublabel: '(multiple)',
|
||||
disabled: false
|
||||
});
|
||||
expect(enabledItems[1]).toMatchObject({
|
||||
label: 'Start Cap ',
|
||||
sublabel: '(none)'
|
||||
});
|
||||
expect(enabledItems[2]).toMatchObject({
|
||||
label: 'End Cap ',
|
||||
sublabel: '(\uE0BC - Diagonal)'
|
||||
});
|
||||
expect(enabledItems[3]).toMatchObject({
|
||||
label: 'Themes ',
|
||||
sublabel: '(Custom)'
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,160 @@
|
||||
import { render } from 'ink';
|
||||
import { PassThrough } from 'node:stream';
|
||||
import React from 'react';
|
||||
import {
|
||||
afterEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi
|
||||
} from 'vitest';
|
||||
|
||||
import { DEFAULT_SETTINGS } from '../../../types/Settings';
|
||||
import { getPowerlineThemes } from '../../../utils/colors';
|
||||
import {
|
||||
PowerlineThemeSelector,
|
||||
applyCustomPowerlineTheme,
|
||||
buildPowerlineThemeItems,
|
||||
type PowerlineThemeSelectorProps
|
||||
} from '../PowerlineThemeSelector';
|
||||
|
||||
class MockTtyStream extends PassThrough {
|
||||
isTTY = true;
|
||||
columns = 120;
|
||||
rows = 40;
|
||||
|
||||
setRawMode() {
|
||||
return this;
|
||||
}
|
||||
|
||||
ref() {
|
||||
return this;
|
||||
}
|
||||
|
||||
unref() {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
function createMockStdin(): NodeJS.ReadStream {
|
||||
return new MockTtyStream() as unknown as NodeJS.ReadStream;
|
||||
}
|
||||
|
||||
function createMockStdout(): NodeJS.WriteStream {
|
||||
return new MockTtyStream() as unknown as NodeJS.WriteStream;
|
||||
}
|
||||
|
||||
function flushInk() {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, 25);
|
||||
});
|
||||
}
|
||||
|
||||
describe('PowerlineThemeSelector helpers', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('builds powerline theme list items with original theme sublabels', () => {
|
||||
const items = buildPowerlineThemeItems(['gruvbox', 'onedark'], 'onedark');
|
||||
|
||||
expect(items).toHaveLength(2);
|
||||
expect(items[0]).toMatchObject({
|
||||
label: 'Gruvbox',
|
||||
value: 'gruvbox'
|
||||
});
|
||||
expect(items[1]).toMatchObject({
|
||||
label: 'One Dark',
|
||||
sublabel: '(original)',
|
||||
value: 'onedark'
|
||||
});
|
||||
});
|
||||
|
||||
it('copies a built-in theme into widget colors and switches to custom mode', () => {
|
||||
const settings = {
|
||||
...DEFAULT_SETTINGS,
|
||||
colorLevel: 2 as const,
|
||||
powerline: {
|
||||
...DEFAULT_SETTINGS.powerline,
|
||||
theme: 'gruvbox'
|
||||
}
|
||||
};
|
||||
|
||||
const updatedSettings = applyCustomPowerlineTheme(settings, 'gruvbox');
|
||||
|
||||
expect(updatedSettings).not.toBeNull();
|
||||
expect(updatedSettings?.powerline.theme).toBe('custom');
|
||||
expect(updatedSettings?.lines[0]?.[0]).toMatchObject({
|
||||
color: 'ansi256:16',
|
||||
backgroundColor: 'ansi256:167'
|
||||
});
|
||||
expect(updatedSettings?.lines[0]?.[1]).toEqual(settings.lines[0]?.[1]);
|
||||
expect(updatedSettings?.lines[0]?.[2]).toMatchObject({
|
||||
color: 'ansi256:235',
|
||||
backgroundColor: 'ansi256:214'
|
||||
});
|
||||
});
|
||||
|
||||
it('returns null when the requested theme cannot be customized', () => {
|
||||
expect(applyCustomPowerlineTheme(DEFAULT_SETTINGS, 'custom')).toBeNull();
|
||||
expect(applyCustomPowerlineTheme(DEFAULT_SETTINGS, 'missing-theme')).toBeNull();
|
||||
});
|
||||
|
||||
it('previews the highlighted theme once without triggering update-depth warnings', async () => {
|
||||
const themes = getPowerlineThemes();
|
||||
|
||||
expect(themes.length).toBeGreaterThan(1);
|
||||
|
||||
const stdin = createMockStdin();
|
||||
const stdout = createMockStdout();
|
||||
const stderr = createMockStdout();
|
||||
const onUpdate = vi.fn<PowerlineThemeSelectorProps['onUpdate']>();
|
||||
const onBack = vi.fn();
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
||||
const instance = render(
|
||||
React.createElement(PowerlineThemeSelector, {
|
||||
settings: {
|
||||
...DEFAULT_SETTINGS,
|
||||
powerline: {
|
||||
...DEFAULT_SETTINGS.powerline,
|
||||
enabled: true,
|
||||
theme: themes[0]
|
||||
}
|
||||
},
|
||||
onUpdate,
|
||||
onBack
|
||||
}),
|
||||
{
|
||||
stdin,
|
||||
stdout,
|
||||
stderr,
|
||||
debug: true,
|
||||
exitOnCtrlC: false,
|
||||
patchConsole: false
|
||||
}
|
||||
);
|
||||
|
||||
try {
|
||||
await flushInk();
|
||||
expect(onUpdate).not.toHaveBeenCalled();
|
||||
|
||||
stdin.write('\u001B[B');
|
||||
await flushInk();
|
||||
|
||||
expect(onUpdate).toHaveBeenCalledTimes(1);
|
||||
expect(onUpdate.mock.calls[0]?.[0]?.powerline.theme).toBe(themes[1]);
|
||||
|
||||
const maximumUpdateDepthWarnings = consoleErrorSpy.mock.calls.filter((call) => {
|
||||
return call.some(arg => typeof arg === 'string' && arg.includes('Maximum update depth exceeded'));
|
||||
});
|
||||
|
||||
expect(maximumUpdateDepthWarnings).toHaveLength(0);
|
||||
} finally {
|
||||
instance.unmount();
|
||||
instance.cleanup();
|
||||
stdin.destroy();
|
||||
stdout.destroy();
|
||||
stderr.destroy();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
describe,
|
||||
expect,
|
||||
it
|
||||
} from 'vitest';
|
||||
|
||||
import {
|
||||
buildTerminalOptionsItems,
|
||||
getNextColorLevel,
|
||||
shouldWarnOnColorLevelChange
|
||||
} from '../TerminalOptionsMenu';
|
||||
|
||||
describe('TerminalOptionsMenu helpers', () => {
|
||||
it('cycles color levels in order', () => {
|
||||
expect(getNextColorLevel(0)).toBe(1);
|
||||
expect(getNextColorLevel(1)).toBe(2);
|
||||
expect(getNextColorLevel(2)).toBe(3);
|
||||
expect(getNextColorLevel(3)).toBe(0);
|
||||
});
|
||||
|
||||
it('warns only when custom colors would be lost', () => {
|
||||
expect(shouldWarnOnColorLevelChange(2, 3, true)).toBe(true);
|
||||
expect(shouldWarnOnColorLevelChange(3, 0, true)).toBe(true);
|
||||
expect(shouldWarnOnColorLevelChange(2, 2, true)).toBe(false);
|
||||
expect(shouldWarnOnColorLevelChange(1, 2, true)).toBe(false);
|
||||
expect(shouldWarnOnColorLevelChange(3, 0, false)).toBe(false);
|
||||
});
|
||||
|
||||
it('builds terminal options list items with the current color level label', () => {
|
||||
const items = buildTerminalOptionsItems(2);
|
||||
|
||||
expect(items).toHaveLength(2);
|
||||
expect(items[0]).toMatchObject({
|
||||
label: '◱ Terminal Width',
|
||||
value: 'width'
|
||||
});
|
||||
expect(items[1]).toMatchObject({
|
||||
label: '▓ Color Level',
|
||||
sublabel: '(256 Color (default))',
|
||||
value: 'colorLevel'
|
||||
});
|
||||
expect(items[1]?.description).toContain('Truecolor');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,169 @@
|
||||
import { render } from 'ink';
|
||||
import { PassThrough } from 'node:stream';
|
||||
import React from 'react';
|
||||
import {
|
||||
afterEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi
|
||||
} from 'vitest';
|
||||
|
||||
import { DEFAULT_SETTINGS } from '../../../types/Settings';
|
||||
import {
|
||||
TerminalWidthMenu,
|
||||
buildTerminalWidthItems,
|
||||
getTerminalWidthSelectionIndex,
|
||||
validateCompactThresholdInput
|
||||
} from '../TerminalWidthMenu';
|
||||
|
||||
class MockTtyStream extends PassThrough {
|
||||
isTTY = true;
|
||||
columns = 120;
|
||||
rows = 40;
|
||||
|
||||
setRawMode() {
|
||||
return this;
|
||||
}
|
||||
|
||||
ref() {
|
||||
return this;
|
||||
}
|
||||
|
||||
unref() {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
interface CapturedWriteStream extends NodeJS.WriteStream {
|
||||
clearOutput: () => void;
|
||||
getOutput: () => string;
|
||||
}
|
||||
|
||||
function createMockStdin(): NodeJS.ReadStream {
|
||||
return new MockTtyStream() as unknown as NodeJS.ReadStream;
|
||||
}
|
||||
|
||||
function createMockStdout(): CapturedWriteStream {
|
||||
const stream = new MockTtyStream();
|
||||
const chunks: string[] = [];
|
||||
|
||||
stream.on('data', (chunk: Buffer | string) => {
|
||||
chunks.push(chunk.toString());
|
||||
});
|
||||
|
||||
return Object.assign(stream as unknown as NodeJS.WriteStream, {
|
||||
clearOutput() {
|
||||
chunks.length = 0;
|
||||
},
|
||||
getOutput() {
|
||||
return chunks.join('');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function flushInk() {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, 25);
|
||||
});
|
||||
}
|
||||
|
||||
describe('TerminalWidthMenu helpers', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('validates compact threshold input', () => {
|
||||
expect(validateCompactThresholdInput('')).toBe('Please enter a valid number');
|
||||
expect(validateCompactThresholdInput('0')).toBe('Value must be between 1 and 99 (you entered 0)');
|
||||
expect(validateCompactThresholdInput('100')).toBe('Value must be between 1 and 99 (you entered 100)');
|
||||
expect(validateCompactThresholdInput('42')).toBeNull();
|
||||
});
|
||||
|
||||
it('builds terminal width menu items with active and threshold sublabels', () => {
|
||||
const items = buildTerminalWidthItems('full-until-compact', 60);
|
||||
|
||||
expect(items).toHaveLength(3);
|
||||
expect(items[0]).toMatchObject({
|
||||
label: 'Full width always',
|
||||
value: 'full'
|
||||
});
|
||||
expect(items[1]).toMatchObject({
|
||||
label: 'Full width minus 40',
|
||||
sublabel: '(default)',
|
||||
value: 'full-minus-40'
|
||||
});
|
||||
expect(items[2]).toMatchObject({
|
||||
label: 'Full width until compact',
|
||||
sublabel: '(threshold 60%, active)',
|
||||
value: 'full-until-compact'
|
||||
});
|
||||
expect(items[2]?.description).toContain('60%');
|
||||
});
|
||||
|
||||
it('returns the current option index for list selection', () => {
|
||||
expect(getTerminalWidthSelectionIndex('full')).toBe(0);
|
||||
expect(getTerminalWidthSelectionIndex('full-minus-40')).toBe(1);
|
||||
expect(getTerminalWidthSelectionIndex('full-until-compact')).toBe(2);
|
||||
});
|
||||
|
||||
it('keeps full-until-compact selected after confirming the threshold prompt', async () => {
|
||||
const stdin = createMockStdin();
|
||||
const stdout = createMockStdout();
|
||||
const stderr = createMockStdout();
|
||||
const onUpdate = vi.fn();
|
||||
const onBack = vi.fn();
|
||||
const instance = render(
|
||||
React.createElement(TerminalWidthMenu, {
|
||||
settings: {
|
||||
...DEFAULT_SETTINGS,
|
||||
flexMode: 'full',
|
||||
compactThreshold: 60
|
||||
},
|
||||
onUpdate,
|
||||
onBack
|
||||
}),
|
||||
{
|
||||
stdin,
|
||||
stdout,
|
||||
stderr,
|
||||
debug: true,
|
||||
exitOnCtrlC: false,
|
||||
patchConsole: false
|
||||
}
|
||||
);
|
||||
|
||||
try {
|
||||
await flushInk();
|
||||
stdin.write('\u001B[B');
|
||||
await flushInk();
|
||||
stdin.write('\u001B[B');
|
||||
await flushInk();
|
||||
stdin.write('\r');
|
||||
await flushInk();
|
||||
|
||||
expect(stdout.getOutput()).toContain('Enter compact threshold (1-99):');
|
||||
|
||||
stdout.clearOutput();
|
||||
|
||||
stdin.write('\r');
|
||||
await flushInk();
|
||||
|
||||
expect(onUpdate).toHaveBeenCalledWith(expect.objectContaining({
|
||||
flexMode: 'full-until-compact',
|
||||
compactThreshold: 60
|
||||
}));
|
||||
|
||||
const output = stdout.getOutput();
|
||||
|
||||
expect(output).toContain('▶ Full width until compact');
|
||||
expect(output).not.toContain('▶ Full width always');
|
||||
} finally {
|
||||
instance.unmount();
|
||||
instance.cleanup();
|
||||
stdin.destroy();
|
||||
stdout.destroy();
|
||||
stderr.destroy();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -166,7 +166,7 @@ describe('items-editor input handlers', () => {
|
||||
setMoveMode: vi.fn(),
|
||||
setShowClearConfirm: vi.fn(),
|
||||
openWidgetPicker: vi.fn(),
|
||||
getVisibleCustomKeybinds: widgetImpl => widgetImpl.getCustomKeybinds ? widgetImpl.getCustomKeybinds() : [],
|
||||
getCustomKeybindsForWidget: (widgetImpl, widget) => widgetImpl.getCustomKeybinds ? widgetImpl.getCustomKeybinds(widget) : [],
|
||||
setCustomEditorWidget: vi.fn()
|
||||
});
|
||||
|
||||
@@ -192,7 +192,7 @@ describe('items-editor input handlers', () => {
|
||||
setMoveMode: vi.fn(),
|
||||
setShowClearConfirm: vi.fn(),
|
||||
openWidgetPicker: vi.fn(),
|
||||
getVisibleCustomKeybinds: widgetImpl => widgetImpl.getCustomKeybinds ? widgetImpl.getCustomKeybinds() : [],
|
||||
getCustomKeybindsForWidget: (widgetImpl, widget) => widgetImpl.getCustomKeybinds ? widgetImpl.getCustomKeybinds(widget) : [],
|
||||
setCustomEditorWidget: vi.fn()
|
||||
});
|
||||
|
||||
@@ -218,11 +218,68 @@ describe('items-editor input handlers', () => {
|
||||
setMoveMode: vi.fn(),
|
||||
setShowClearConfirm: vi.fn(),
|
||||
openWidgetPicker: vi.fn(),
|
||||
getVisibleCustomKeybinds: widgetImpl => widgetImpl.getCustomKeybinds ? widgetImpl.getCustomKeybinds() : [],
|
||||
getCustomKeybindsForWidget: (widgetImpl, widget) => widgetImpl.getCustomKeybinds ? widgetImpl.getCustomKeybinds(widget) : [],
|
||||
setCustomEditorWidget: vi.fn()
|
||||
});
|
||||
|
||||
const updated = onUpdate.mock.calls[0]?.[0] as WidgetItem[] | undefined;
|
||||
expect(updated?.[0]?.metadata?.display).toBe('progress');
|
||||
});
|
||||
|
||||
it('uses v to cycle skills widget mode', () => {
|
||||
const widgets: WidgetItem[] = [
|
||||
{ id: '1', type: 'skills' }
|
||||
];
|
||||
const onUpdate = vi.fn();
|
||||
|
||||
handleNormalInputMode({
|
||||
input: 'v',
|
||||
key: {},
|
||||
widgets,
|
||||
selectedIndex: 0,
|
||||
separatorChars: ['|', '-'],
|
||||
onBack: vi.fn(),
|
||||
onUpdate,
|
||||
setSelectedIndex: vi.fn(),
|
||||
setMoveMode: vi.fn(),
|
||||
setShowClearConfirm: vi.fn(),
|
||||
openWidgetPicker: vi.fn(),
|
||||
getCustomKeybindsForWidget: (widgetImpl, widget) => widgetImpl.getCustomKeybinds ? widgetImpl.getCustomKeybinds(widget) : [],
|
||||
setCustomEditorWidget: vi.fn()
|
||||
});
|
||||
|
||||
const updated = onUpdate.mock.calls[0]?.[0] as WidgetItem[] | undefined;
|
||||
expect(updated?.[0]?.metadata?.mode).toBe('count');
|
||||
});
|
||||
|
||||
it('opens custom editor for skills list limit action', () => {
|
||||
const widgets: WidgetItem[] = [
|
||||
{ id: '1', type: 'skills', metadata: { mode: 'list' } }
|
||||
];
|
||||
const onUpdate = vi.fn();
|
||||
const setCustomEditorWidget = vi.fn();
|
||||
|
||||
handleNormalInputMode({
|
||||
input: 'l',
|
||||
key: {},
|
||||
widgets,
|
||||
selectedIndex: 0,
|
||||
separatorChars: ['|', '-'],
|
||||
onBack: vi.fn(),
|
||||
onUpdate,
|
||||
setSelectedIndex: vi.fn(),
|
||||
setMoveMode: vi.fn(),
|
||||
setShowClearConfirm: vi.fn(),
|
||||
openWidgetPicker: vi.fn(),
|
||||
getCustomKeybindsForWidget: (widgetImpl, widget) => widgetImpl.getCustomKeybinds ? widgetImpl.getCustomKeybinds(widget) : [],
|
||||
setCustomEditorWidget
|
||||
});
|
||||
|
||||
expect(onUpdate).not.toHaveBeenCalled();
|
||||
const customEditorState = setCustomEditorWidget.mock.calls[0]?.[0] as
|
||||
| { action?: string; widget?: WidgetItem }
|
||||
| undefined;
|
||||
expect(customEditorState?.action).toBe('edit-list-limit');
|
||||
expect(customEditorState?.widget?.type).toBe('skills');
|
||||
});
|
||||
});
|
||||
@@ -338,7 +338,7 @@ export interface HandleNormalInputModeArgs {
|
||||
setMoveMode: (moveMode: boolean) => void;
|
||||
setShowClearConfirm: (show: boolean) => void;
|
||||
openWidgetPicker: (action: WidgetPickerAction) => void;
|
||||
getVisibleCustomKeybinds: (widgetImpl: Widget, widget: WidgetItem) => CustomKeybind[];
|
||||
getCustomKeybindsForWidget: (widgetImpl: Widget, widget: WidgetItem) => CustomKeybind[];
|
||||
setCustomEditorWidget: (state: CustomEditorWidgetState | null) => void;
|
||||
}
|
||||
|
||||
@@ -354,7 +354,7 @@ export function handleNormalInputMode({
|
||||
setMoveMode,
|
||||
setShowClearConfirm,
|
||||
openWidgetPicker,
|
||||
getVisibleCustomKeybinds,
|
||||
getCustomKeybindsForWidget,
|
||||
setCustomEditorWidget
|
||||
}: HandleNormalInputModeArgs): void {
|
||||
if (key.upArrow && widgets.length > 0) {
|
||||
@@ -436,7 +436,7 @@ export function handleNormalInputMode({
|
||||
return;
|
||||
}
|
||||
|
||||
const customKeybinds = getVisibleCustomKeybinds(widgetImpl, currentWidget);
|
||||
const customKeybinds = getCustomKeybindsForWidget(widgetImpl, currentWidget);
|
||||
const matchedKeybind = customKeybinds.find(kb => kb.key === input);
|
||||
|
||||
if (matchedKeybind && !key.ctrl) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export interface ClaudeSettings {
|
||||
effortLevel?: 'low' | 'medium' | 'high' | 'max';
|
||||
permissions?: {
|
||||
allow?: string[];
|
||||
deny?: string[];
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import type { BlockMetrics } from '../types';
|
||||
import type {
|
||||
BlockMetrics,
|
||||
SkillsMetrics
|
||||
} from '../types';
|
||||
|
||||
import type { SpeedMetrics } from './SpeedMetrics';
|
||||
import type { StatusJSON } from './StatusJSON';
|
||||
@@ -13,7 +16,7 @@ export interface RenderUsageData {
|
||||
extraUsageLimit?: number;
|
||||
extraUsageUsed?: number;
|
||||
extraUsageUtilization?: number;
|
||||
error?: 'no-credentials' | 'timeout' | 'api-error' | 'parse-error';
|
||||
error?: 'no-credentials' | 'timeout' | 'rate-limited' | 'api-error' | 'parse-error';
|
||||
}
|
||||
|
||||
export interface RenderContext {
|
||||
@@ -24,6 +27,7 @@ export interface RenderContext {
|
||||
usageData?: RenderUsageData | null;
|
||||
sessionDuration?: string | null;
|
||||
blockMetrics?: BlockMetrics | null;
|
||||
skillsMetrics?: SkillsMetrics | null;
|
||||
terminalWidth?: number | null;
|
||||
isPreview?: boolean;
|
||||
lineIndex?: number; // Index of the current line being rendered (for theme cycling)
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
export interface SkillInvocation {
|
||||
timestamp: string;
|
||||
session_id: string;
|
||||
skill: string;
|
||||
source: string;
|
||||
}
|
||||
|
||||
export interface SkillsMetrics {
|
||||
totalInvocations: number;
|
||||
uniqueSkills: string[];
|
||||
lastSkill: string | null;
|
||||
}
|
||||
@@ -54,7 +54,8 @@ export const StatusJSONSchema = z.looseObject({
|
||||
]).nullable().optional(),
|
||||
used_percentage: CoercedNumberSchema.nullable().optional(),
|
||||
remaining_percentage: CoercedNumberSchema.nullable().optional()
|
||||
}).nullable().optional()
|
||||
}).nullable().optional(),
|
||||
vim: z.object({ mode: z.string().optional() }).nullable().optional()
|
||||
});
|
||||
|
||||
export type StatusJSON = z.infer<typeof StatusJSONSchema>;
|
||||
+1
-1
@@ -37,7 +37,7 @@ export interface Widget {
|
||||
getCategory(): string;
|
||||
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay;
|
||||
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null;
|
||||
getCustomKeybinds?(): CustomKeybind[];
|
||||
getCustomKeybinds?(item?: WidgetItem): CustomKeybind[];
|
||||
renderEditor?(props: WidgetEditorProps): React.ReactElement | null;
|
||||
supportsRawValue(): boolean;
|
||||
supportsColors(item: WidgetItem): boolean;
|
||||
|
||||
@@ -58,4 +58,15 @@ describe('StatusJSONSchema numeric coercion', () => {
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it('accepts null vim payloads', () => {
|
||||
const result = StatusJSONSchema.safeParse({ vim: null });
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
if (!result.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
expect(result.data.vim).toBeNull();
|
||||
});
|
||||
});
|
||||
+2
-1
@@ -18,4 +18,5 @@ export type { PowerlineFontStatus } from './PowerlineFontStatus';
|
||||
export type { ClaudeSettings } from './ClaudeSettings';
|
||||
export type { ColorEntry } from './ColorEntry';
|
||||
export type { BlockMetrics } from './BlockMetrics';
|
||||
export type { SpeedMetrics } from './SpeedMetrics';
|
||||
export type { SpeedMetrics } from './SpeedMetrics';
|
||||
export type { SkillInvocation, SkillsMetrics } from './SkillsMetrics';
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
vi
|
||||
} from 'vitest';
|
||||
|
||||
import { DEFAULT_SETTINGS } from '../../types/Settings';
|
||||
import {
|
||||
CCSTATUSLINE_COMMANDS,
|
||||
getClaudeSettingsPath,
|
||||
@@ -143,6 +144,36 @@ describe('buildCommand via installStatusLine', () => {
|
||||
await installStatusLine(true);
|
||||
expect(readInstalledCommand()).toBe(`${CCSTATUSLINE_COMMANDS.BUNX} --config '/my path/settings.json'`);
|
||||
});
|
||||
|
||||
it('should sync hooks on install when settings include hook-enabled widgets', async () => {
|
||||
const configPath = path.join(testClaudeConfigDir, 'ccstatusline-settings.json');
|
||||
initConfigPath(configPath);
|
||||
const settingsWithSkills = {
|
||||
...DEFAULT_SETTINGS,
|
||||
lines: [[{ id: 'skills-1', type: 'skills' }], [], []]
|
||||
};
|
||||
fs.writeFileSync(configPath, JSON.stringify(settingsWithSkills, null, 2), 'utf-8');
|
||||
|
||||
await installStatusLine(false);
|
||||
|
||||
const installedCommand = `${CCSTATUSLINE_COMMANDS.NPM} --config ${configPath}`;
|
||||
const claudeSettings = await loadClaudeSettings();
|
||||
expect(claudeSettings.statusLine?.command).toBe(installedCommand);
|
||||
const hooks = (claudeSettings.hooks ?? {}) as Record<string, unknown[]>;
|
||||
expect(hooks.PreToolUse).toEqual([
|
||||
{
|
||||
_tag: 'ccstatusline-managed',
|
||||
matcher: 'Skill',
|
||||
hooks: [{ type: 'command', command: `${installedCommand} --hook` }]
|
||||
}
|
||||
]);
|
||||
expect(hooks.UserPromptSubmit).toEqual([
|
||||
{
|
||||
_tag: 'ccstatusline-managed',
|
||||
hooks: [{ type: 'command', command: `${installedCommand} --hook` }]
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('backup and error handling behavior', () => {
|
||||
@@ -257,6 +288,52 @@ describe('backup and error handling behavior', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('uninstallStatusLine should remove all managed hooks', async () => {
|
||||
writeRawClaudeSettings(JSON.stringify({
|
||||
statusLine: {
|
||||
type: 'command',
|
||||
command: CCSTATUSLINE_COMMANDS.NPM,
|
||||
padding: 0
|
||||
},
|
||||
hooks: {
|
||||
PreToolUse: [
|
||||
{
|
||||
_tag: 'ccstatusline-managed',
|
||||
matcher: 'Skill',
|
||||
hooks: [{ type: 'command', command: `${CCSTATUSLINE_COMMANDS.NPM} --hook` }]
|
||||
},
|
||||
{
|
||||
matcher: 'Other',
|
||||
hooks: [{ type: 'command', command: 'keep-me' }]
|
||||
}
|
||||
],
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
_tag: 'ccstatusline-managed',
|
||||
hooks: [{ type: 'command', command: `${CCSTATUSLINE_COMMANDS.NPM} --hook` }]
|
||||
}
|
||||
]
|
||||
}
|
||||
}));
|
||||
|
||||
await uninstallStatusLine();
|
||||
|
||||
const settingsPath = getClaudeSettingsPath();
|
||||
const updated = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')) as {
|
||||
statusLine?: unknown;
|
||||
hooks?: Record<string, unknown[]>;
|
||||
};
|
||||
expect(updated.statusLine).toBeUndefined();
|
||||
expect(updated.hooks).toEqual({
|
||||
PreToolUse: [
|
||||
{
|
||||
matcher: 'Other',
|
||||
hooks: [{ type: 'command', command: 'keep-me' }]
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
|
||||
it('getExistingStatusLine should return null when settings cannot be loaded', async () => {
|
||||
writeRawClaudeSettings('{ invalid json');
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from '../../types/Settings';
|
||||
|
||||
const MOCK_HOME_DIR = '/tmp/ccstatusline-config-test-home';
|
||||
const ORIGINAL_CLAUDE_CONFIG_DIR = process.env.CLAUDE_CONFIG_DIR;
|
||||
|
||||
let loadSettings: () => Promise<Settings>;
|
||||
let saveSettings: (settings: Settings) => Promise<void>;
|
||||
@@ -34,6 +35,10 @@ function getSettingsPaths(): { configDir: string; settingsPath: string; backupPa
|
||||
};
|
||||
}
|
||||
|
||||
function getClaudeConfigDir(): string {
|
||||
return path.join(MOCK_HOME_DIR, '.claude');
|
||||
}
|
||||
|
||||
describe('config utilities', () => {
|
||||
beforeAll(async () => {
|
||||
const configModule = await import('../config');
|
||||
@@ -44,6 +49,7 @@ describe('config utilities', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
fs.rmSync(MOCK_HOME_DIR, { recursive: true, force: true });
|
||||
process.env.CLAUDE_CONFIG_DIR = getClaudeConfigDir();
|
||||
const { settingsPath } = getSettingsPaths();
|
||||
initConfigPath(settingsPath);
|
||||
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
||||
@@ -55,6 +61,11 @@ describe('config utilities', () => {
|
||||
|
||||
afterAll(() => {
|
||||
fs.rmSync(MOCK_HOME_DIR, { recursive: true, force: true });
|
||||
if (ORIGINAL_CLAUDE_CONFIG_DIR === undefined) {
|
||||
delete process.env.CLAUDE_CONFIG_DIR;
|
||||
} else {
|
||||
process.env.CLAUDE_CONFIG_DIR = ORIGINAL_CLAUDE_CONFIG_DIR;
|
||||
}
|
||||
initConfigPath();
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import {
|
||||
afterAll,
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it
|
||||
} from 'vitest';
|
||||
|
||||
import { DEFAULT_SETTINGS } from '../../types/Settings';
|
||||
import { syncWidgetHooks } from '../hooks';
|
||||
|
||||
const ORIGINAL_CLAUDE_CONFIG_DIR = process.env.CLAUDE_CONFIG_DIR;
|
||||
let testClaudeConfigDir = '';
|
||||
|
||||
function getClaudeSettingsPath(): string {
|
||||
return path.join(testClaudeConfigDir, 'settings.json');
|
||||
}
|
||||
|
||||
describe('syncWidgetHooks', () => {
|
||||
beforeEach(() => {
|
||||
testClaudeConfigDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccstatusline-hooks-'));
|
||||
process.env.CLAUDE_CONFIG_DIR = testClaudeConfigDir;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (testClaudeConfigDir) {
|
||||
fs.rmSync(testClaudeConfigDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (ORIGINAL_CLAUDE_CONFIG_DIR === undefined) {
|
||||
delete process.env.CLAUDE_CONFIG_DIR;
|
||||
} else {
|
||||
process.env.CLAUDE_CONFIG_DIR = ORIGINAL_CLAUDE_CONFIG_DIR;
|
||||
}
|
||||
});
|
||||
|
||||
it('removes managed hooks and persists cleanup when status line is unset', async () => {
|
||||
const settingsPath = getClaudeSettingsPath();
|
||||
fs.writeFileSync(settingsPath, JSON.stringify({
|
||||
hooks: {
|
||||
PreToolUse: [
|
||||
{
|
||||
_tag: 'ccstatusline-managed',
|
||||
matcher: 'Skill',
|
||||
hooks: [{ type: 'command', command: 'old-command --hook' }]
|
||||
},
|
||||
{
|
||||
matcher: 'Other',
|
||||
hooks: [{ type: 'command', command: 'keep-command' }]
|
||||
}
|
||||
],
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
_tag: 'ccstatusline-managed',
|
||||
hooks: [{ type: 'command', command: 'old-command --hook' }]
|
||||
}
|
||||
]
|
||||
}
|
||||
}, null, 2), 'utf-8');
|
||||
|
||||
await syncWidgetHooks(DEFAULT_SETTINGS);
|
||||
|
||||
const saved = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')) as { hooks?: Record<string, unknown[]> };
|
||||
expect(saved.hooks).toEqual({
|
||||
PreToolUse: [
|
||||
{
|
||||
matcher: 'Other',
|
||||
hooks: [{ type: 'command', command: 'keep-command' }]
|
||||
}
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
describe,
|
||||
expect,
|
||||
it
|
||||
} from 'vitest';
|
||||
|
||||
import {
|
||||
buildIdeFileUrl,
|
||||
encodeGitRefForUrlPath,
|
||||
parseGitHubBaseUrl
|
||||
} from '../hyperlink';
|
||||
|
||||
describe('parseGitHubBaseUrl', () => {
|
||||
it('supports scp-style SSH remotes', () => {
|
||||
expect(parseGitHubBaseUrl('git@github.com:owner/repo.git')).toBe('https://github.com/owner/repo');
|
||||
});
|
||||
|
||||
it('supports ssh URL remotes', () => {
|
||||
expect(parseGitHubBaseUrl('ssh://git@github.com/owner/repo.git')).toBe('https://github.com/owner/repo');
|
||||
});
|
||||
|
||||
it('supports credentialed HTTPS remotes', () => {
|
||||
expect(parseGitHubBaseUrl('https://token@github.com/owner/repo.git')).toBe('https://github.com/owner/repo');
|
||||
});
|
||||
|
||||
it('rejects non-GitHub remotes', () => {
|
||||
expect(parseGitHubBaseUrl('https://gitlab.com/owner/repo.git')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('encodeGitRefForUrlPath', () => {
|
||||
it('encodes reserved characters while preserving branch separators', () => {
|
||||
expect(encodeGitRefForUrlPath('feature/issue#1')).toBe('feature/issue%231');
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildIdeFileUrl', () => {
|
||||
it('builds encoded IDE links for POSIX paths', () => {
|
||||
expect(buildIdeFileUrl('/Users/example/my repo#1', 'cursor')).toBe('cursor://file/Users/example/my%20repo%231');
|
||||
});
|
||||
|
||||
it('builds IDE links for Windows drive-letter paths', () => {
|
||||
expect(buildIdeFileUrl('C:/Work/my repo#1', 'vscode')).toBe('vscode://file/C:/Work/my%20repo%231');
|
||||
});
|
||||
|
||||
it('builds IDE links for UNC paths', () => {
|
||||
expect(buildIdeFileUrl('\\\\server\\share\\my repo', 'cursor')).toBe('cursor://file//server/share/my%20repo');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi
|
||||
} from 'vitest';
|
||||
|
||||
import {
|
||||
getSkillsFilePath,
|
||||
getSkillsMetrics
|
||||
} from '../skills';
|
||||
|
||||
let testHomeDir = '';
|
||||
|
||||
function writeSkillsLog(sessionId: string, lines: string[]): void {
|
||||
const skillsPath = getSkillsFilePath(sessionId);
|
||||
fs.mkdirSync(path.dirname(skillsPath), { recursive: true });
|
||||
fs.writeFileSync(skillsPath, lines.join('\n'), 'utf-8');
|
||||
}
|
||||
|
||||
describe('skills metrics', () => {
|
||||
beforeEach(() => {
|
||||
testHomeDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccstatusline-home-'));
|
||||
vi.spyOn(os, 'homedir').mockReturnValue(testHomeDir);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
if (testHomeDir) {
|
||||
fs.rmSync(testHomeDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('uses ~/.cache/ccstatusline/skills path for skill logs', () => {
|
||||
expect(getSkillsFilePath('session-1')).toBe(
|
||||
path.join(testHomeDir, '.cache', 'ccstatusline', 'skills', 'skills-session-1.jsonl')
|
||||
);
|
||||
});
|
||||
|
||||
it('returns total, unique (most-recent-first), and last skill from a valid log', () => {
|
||||
writeSkillsLog('session-1', [
|
||||
JSON.stringify({ skill: 'commit', session_id: 'session-1' }),
|
||||
JSON.stringify({ skill: 'review-pr', session_id: 'session-1' }),
|
||||
JSON.stringify({ skill: 'lint', session_id: 'session-1' }),
|
||||
JSON.stringify({ skill: 'commit', session_id: 'session-1' })
|
||||
]);
|
||||
|
||||
expect(getSkillsMetrics('session-1')).toEqual({
|
||||
totalInvocations: 4,
|
||||
uniqueSkills: ['commit', 'lint', 'review-pr'],
|
||||
lastSkill: 'commit'
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -15,52 +15,36 @@ interface UsageProbeResult {
|
||||
lockExists: boolean;
|
||||
cacheExists: boolean;
|
||||
requestCount: number;
|
||||
proxyAgentConfigured: boolean;
|
||||
requestHost: string | null;
|
||||
lockContents: string | null;
|
||||
}
|
||||
|
||||
describe('fetchUsageData error handling', () => {
|
||||
it('preserves root errors within lock window and avoids locking on no-credentials', () => {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ccstatusline-usage-test-'));
|
||||
const probeScriptPath = path.join(tempRoot, 'probe-usage.mjs');
|
||||
const usageModulePath = fileURLToPath(new URL('../usage.ts', import.meta.url));
|
||||
interface TokenHome {
|
||||
bin: string;
|
||||
claudeConfig: string;
|
||||
home: string;
|
||||
}
|
||||
|
||||
const noCredentialsHome = path.join(tempRoot, 'home-no-credentials');
|
||||
const apiErrorHome = path.join(tempRoot, 'home-api-error');
|
||||
const apiErrorBin = path.join(tempRoot, 'bin-api-error');
|
||||
const apiErrorClaudeConfig = path.join(tempRoot, 'claude-api-error');
|
||||
const successHome = path.join(tempRoot, 'home-success');
|
||||
const successBin = path.join(tempRoot, 'bin-success');
|
||||
const successClaudeConfig = path.join(tempRoot, 'claude-success');
|
||||
const securityScript = path.join(apiErrorBin, 'security');
|
||||
const successSecurityScript = path.join(successBin, 'security');
|
||||
const credentialsFile = path.join(apiErrorClaudeConfig, '.credentials.json');
|
||||
const successCredentialsFile = path.join(successClaudeConfig, '.credentials.json');
|
||||
const successResponseBody = JSON.stringify({
|
||||
five_hour: {
|
||||
utilization: 42,
|
||||
resets_at: '2030-01-01T00:00:00.000Z'
|
||||
},
|
||||
seven_day: {
|
||||
utilization: 17,
|
||||
resets_at: '2030-01-07T00:00:00.000Z'
|
||||
}
|
||||
});
|
||||
interface ProbeOptions {
|
||||
claudeConfigDir?: string;
|
||||
home: string;
|
||||
httpsProxy?: string;
|
||||
lowercaseHttpsProxy?: string;
|
||||
mode?: 'error' | 'status' | 'success' | 'unexpected';
|
||||
nowMs: number;
|
||||
pathDir?: string;
|
||||
responseBody?: string;
|
||||
responseHeaders?: Record<string, string>;
|
||||
statusCode?: number;
|
||||
}
|
||||
|
||||
fs.mkdirSync(noCredentialsHome, { recursive: true });
|
||||
fs.mkdirSync(apiErrorHome, { recursive: true });
|
||||
fs.mkdirSync(apiErrorBin, { recursive: true });
|
||||
fs.mkdirSync(apiErrorClaudeConfig, { recursive: true });
|
||||
fs.mkdirSync(successHome, { recursive: true });
|
||||
fs.mkdirSync(successBin, { recursive: true });
|
||||
fs.mkdirSync(successClaudeConfig, { recursive: true });
|
||||
function createProbeHarness() {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ccstatusline-usage-test-'));
|
||||
const probeScriptPath = path.join(tempRoot, 'probe-usage.mjs');
|
||||
const usageModulePath = fileURLToPath(new URL('../usage.ts', import.meta.url));
|
||||
|
||||
fs.writeFileSync(securityScript, '#!/bin/sh\necho \'{"claudeAiOauth":{"accessToken":"test-token"}}\'\n');
|
||||
fs.chmodSync(securityScript, 0o755);
|
||||
fs.writeFileSync(credentialsFile, JSON.stringify({ claudeAiOauth: { accessToken: 'test-token' } }));
|
||||
fs.writeFileSync(successSecurityScript, '#!/bin/sh\necho \'{"claudeAiOauth":{"accessToken":"test-token"}}\'\n');
|
||||
fs.chmodSync(successSecurityScript, 0o755);
|
||||
fs.writeFileSync(successCredentialsFile, JSON.stringify({ claudeAiOauth: { accessToken: 'test-token' } }));
|
||||
|
||||
const probeScript = `
|
||||
const probeScript = `
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
@@ -70,16 +54,24 @@ const require = createRequire(import.meta.url);
|
||||
const https = require('https');
|
||||
const mode = process.env.TEST_REQUEST_MODE || 'success';
|
||||
const responseBody = process.env.TEST_RESPONSE_BODY || '';
|
||||
const responseHeaders = JSON.parse(process.env.TEST_RESPONSE_HEADERS_JSON || '{}');
|
||||
const statusCode = Number(process.env.TEST_STATUS_CODE || (mode === 'success' ? '200' : '500'));
|
||||
let requestCount = 0;
|
||||
let proxyAgentConfigured = false;
|
||||
let requestHost = null;
|
||||
|
||||
https.request = (...args) => {
|
||||
requestCount += 1;
|
||||
const callback = args.find(value => typeof value === 'function');
|
||||
const options = args.find(value => value && typeof value === 'object' && !Buffer.isBuffer(value));
|
||||
proxyAgentConfigured = Boolean(options?.agent);
|
||||
requestHost = options?.hostname ?? null;
|
||||
const requestHandlers = new Map();
|
||||
const responseHandlers = new Map();
|
||||
|
||||
const response = {
|
||||
statusCode: mode === 'success' ? 200 : 500,
|
||||
headers: responseHeaders,
|
||||
statusCode,
|
||||
setEncoding() {},
|
||||
on(event, handler) {
|
||||
const existing = responseHandlers.get(event) || [];
|
||||
@@ -106,24 +98,28 @@ https.request = (...args) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode === 'success') {
|
||||
if (callback) {
|
||||
callback(response);
|
||||
}
|
||||
const dataHandlers = responseHandlers.get('data') || [];
|
||||
for (const handler of dataHandlers) {
|
||||
handler(Buffer.from(responseBody));
|
||||
}
|
||||
const endHandlers = responseHandlers.get('end') || [];
|
||||
for (const handler of endHandlers) {
|
||||
handler();
|
||||
if (mode === 'unexpected') {
|
||||
const handlers = requestHandlers.get('error') || [];
|
||||
for (const handler of handlers) {
|
||||
handler(new Error('unexpected request'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const handlers = requestHandlers.get('error') || [];
|
||||
for (const handler of handlers) {
|
||||
handler(new Error('unexpected request'));
|
||||
if (callback) {
|
||||
callback(response);
|
||||
}
|
||||
|
||||
if (responseBody !== '') {
|
||||
const dataHandlers = responseHandlers.get('data') || [];
|
||||
for (const handler of dataHandlers) {
|
||||
handler(responseBody);
|
||||
}
|
||||
}
|
||||
|
||||
const endHandlers = responseHandlers.get('end') || [];
|
||||
for (const handler of endHandlers) {
|
||||
handler();
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -145,63 +141,174 @@ process.stdout.write(JSON.stringify({
|
||||
second,
|
||||
lockExists: fs.existsSync(lockFile),
|
||||
cacheExists: fs.existsSync(cacheFile),
|
||||
requestCount
|
||||
requestCount,
|
||||
proxyAgentConfigured,
|
||||
requestHost,
|
||||
lockContents: fs.existsSync(lockFile) ? fs.readFileSync(lockFile, 'utf8') : null
|
||||
}));
|
||||
`;
|
||||
|
||||
try {
|
||||
fs.writeFileSync(probeScriptPath, probeScript);
|
||||
fs.writeFileSync(probeScriptPath, probeScript);
|
||||
|
||||
const noCredentialsOutput = execFileSync(process.execPath, [probeScriptPath], {
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: noCredentialsHome,
|
||||
PATH: '/nonexistent',
|
||||
TEST_REQUEST_MODE: 'unexpected',
|
||||
TEST_NOW_MS: '2200000000000'
|
||||
}
|
||||
function createEmptyHome(name: string): { home: string } {
|
||||
const home = path.join(tempRoot, `home-${name}`);
|
||||
fs.mkdirSync(home, { recursive: true });
|
||||
return { home };
|
||||
}
|
||||
|
||||
function createTokenHome(name: string): TokenHome {
|
||||
const home = path.join(tempRoot, `home-${name}`);
|
||||
const bin = path.join(tempRoot, `bin-${name}`);
|
||||
const claudeConfig = path.join(tempRoot, `claude-${name}`);
|
||||
const securityScript = path.join(bin, 'security');
|
||||
const credentialsFile = path.join(claudeConfig, '.credentials.json');
|
||||
|
||||
fs.mkdirSync(home, { recursive: true });
|
||||
fs.mkdirSync(bin, { recursive: true });
|
||||
fs.mkdirSync(claudeConfig, { recursive: true });
|
||||
|
||||
fs.writeFileSync(securityScript, '#!/bin/sh\necho \'{"claudeAiOauth":{"accessToken":"test-token"}}\'\n');
|
||||
fs.chmodSync(securityScript, 0o755);
|
||||
fs.writeFileSync(credentialsFile, JSON.stringify({ claudeAiOauth: { accessToken: 'test-token' } }));
|
||||
|
||||
return {
|
||||
bin,
|
||||
claudeConfig,
|
||||
home
|
||||
};
|
||||
}
|
||||
|
||||
function runProbe(options: ProbeOptions): UsageProbeResult {
|
||||
const output = execFileSync(process.execPath, [probeScriptPath], {
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: options.home,
|
||||
PATH: options.pathDir ?? '/nonexistent',
|
||||
TEST_NOW_MS: String(options.nowMs),
|
||||
TEST_REQUEST_MODE: options.mode ?? 'success',
|
||||
TEST_RESPONSE_BODY: options.responseBody ?? '',
|
||||
TEST_RESPONSE_HEADERS_JSON: JSON.stringify(options.responseHeaders ?? {}),
|
||||
TEST_STATUS_CODE: String(options.statusCode ?? (options.mode === 'success' ? 200 : 500)),
|
||||
...(options.claudeConfigDir ? { CLAUDE_CONFIG_DIR: options.claudeConfigDir } : {}),
|
||||
...(options.httpsProxy !== undefined ? { HTTPS_PROXY: options.httpsProxy } : {}),
|
||||
...(options.lowercaseHttpsProxy !== undefined ? { https_proxy: options.lowercaseHttpsProxy } : {})
|
||||
}
|
||||
});
|
||||
|
||||
return JSON.parse(output) as UsageProbeResult;
|
||||
}
|
||||
|
||||
function cleanup(): void {
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
return {
|
||||
cleanup,
|
||||
createEmptyHome,
|
||||
createTokenHome,
|
||||
runProbe
|
||||
};
|
||||
}
|
||||
|
||||
function parseLockContents(lockContents: string | null): { blockedUntil: number; error?: string } | null {
|
||||
return lockContents ? JSON.parse(lockContents) as { blockedUntil: number; error?: string } : null;
|
||||
}
|
||||
|
||||
describe('fetchUsageData error handling', () => {
|
||||
const nowMs = 2200000000000;
|
||||
const successResponseBody = JSON.stringify({
|
||||
five_hour: {
|
||||
utilization: 42,
|
||||
resets_at: '2030-01-01T00:00:00.000Z'
|
||||
},
|
||||
seven_day: {
|
||||
utilization: 17,
|
||||
resets_at: '2030-01-07T00:00:00.000Z'
|
||||
}
|
||||
});
|
||||
const updatedSuccessResponseBody = JSON.stringify({
|
||||
five_hour: {
|
||||
utilization: 55,
|
||||
resets_at: '2030-01-02T00:00:00.000Z'
|
||||
},
|
||||
seven_day: {
|
||||
utilization: 21,
|
||||
resets_at: '2030-01-08T00:00:00.000Z'
|
||||
}
|
||||
});
|
||||
const rateLimitedResponseBody = JSON.stringify({
|
||||
error: {
|
||||
message: 'Rate limited. Please try again later.',
|
||||
type: 'rate_limit_error'
|
||||
}
|
||||
});
|
||||
|
||||
it('preserves root errors within a process and keeps existing proxy and cache behavior', () => {
|
||||
const harness = createProbeHarness();
|
||||
|
||||
try {
|
||||
const noCredentialsHome = harness.createEmptyHome('no-credentials');
|
||||
const apiErrorHome = harness.createTokenHome('api-error');
|
||||
const successHome = harness.createTokenHome('success');
|
||||
const invalidProxyHome = harness.createTokenHome('invalid-proxy');
|
||||
const proxyHome = harness.createTokenHome('proxy');
|
||||
const blankProxyHome = harness.createTokenHome('blank-proxy');
|
||||
const lowercaseProxyHome = harness.createTokenHome('lowercase-proxy');
|
||||
|
||||
const noCredentialsResult = harness.runProbe({
|
||||
home: noCredentialsHome.home,
|
||||
mode: 'unexpected',
|
||||
nowMs
|
||||
});
|
||||
|
||||
const noCredentialsResult = JSON.parse(noCredentialsOutput) as UsageProbeResult;
|
||||
expect(noCredentialsResult.first).toEqual({ error: 'no-credentials' });
|
||||
expect(noCredentialsResult.second).toEqual({ error: 'no-credentials' });
|
||||
expect(noCredentialsResult.lockExists).toBe(false);
|
||||
expect(noCredentialsResult.cacheExists).toBe(false);
|
||||
expect(noCredentialsResult.requestCount).toBe(0);
|
||||
expect(noCredentialsResult.proxyAgentConfigured).toBe(false);
|
||||
|
||||
const apiErrorOutput = execFileSync(process.execPath, [probeScriptPath], {
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: apiErrorHome,
|
||||
PATH: apiErrorBin,
|
||||
CLAUDE_CONFIG_DIR: apiErrorClaudeConfig,
|
||||
TEST_REQUEST_MODE: 'error',
|
||||
TEST_NOW_MS: '2200000000000'
|
||||
}
|
||||
const apiErrorResult = harness.runProbe({
|
||||
claudeConfigDir: apiErrorHome.claudeConfig,
|
||||
home: apiErrorHome.home,
|
||||
mode: 'error',
|
||||
nowMs,
|
||||
pathDir: apiErrorHome.bin
|
||||
});
|
||||
|
||||
const apiErrorResult = JSON.parse(apiErrorOutput) as UsageProbeResult;
|
||||
expect(apiErrorResult.first).toEqual({ error: 'api-error' });
|
||||
expect(apiErrorResult.second).toEqual({ error: 'api-error' });
|
||||
expect(apiErrorResult.cacheExists).toBe(false);
|
||||
expect(apiErrorResult.requestCount).toBe(1);
|
||||
|
||||
const successOutput = execFileSync(process.execPath, [probeScriptPath], {
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: successHome,
|
||||
PATH: successBin,
|
||||
CLAUDE_CONFIG_DIR: successClaudeConfig,
|
||||
TEST_REQUEST_MODE: 'success',
|
||||
TEST_RESPONSE_BODY: successResponseBody,
|
||||
TEST_NOW_MS: '2200000000000'
|
||||
}
|
||||
expect(apiErrorResult.proxyAgentConfigured).toBe(false);
|
||||
expect(apiErrorResult.requestHost).toBe('api.anthropic.com');
|
||||
expect(parseLockContents(apiErrorResult.lockContents)).toEqual({
|
||||
blockedUntil: Math.floor(nowMs / 1000) + 30,
|
||||
error: 'timeout'
|
||||
});
|
||||
|
||||
const genericLockResult = harness.runProbe({
|
||||
claudeConfigDir: apiErrorHome.claudeConfig,
|
||||
home: apiErrorHome.home,
|
||||
mode: 'unexpected',
|
||||
nowMs,
|
||||
pathDir: apiErrorHome.bin
|
||||
});
|
||||
|
||||
expect(genericLockResult.first).toEqual({ error: 'timeout' });
|
||||
expect(genericLockResult.second).toEqual({ error: 'timeout' });
|
||||
expect(genericLockResult.requestCount).toBe(0);
|
||||
|
||||
const successResult = harness.runProbe({
|
||||
claudeConfigDir: successHome.claudeConfig,
|
||||
home: successHome.home,
|
||||
mode: 'success',
|
||||
nowMs,
|
||||
pathDir: successHome.bin,
|
||||
responseBody: successResponseBody
|
||||
});
|
||||
|
||||
const successResult = JSON.parse(successOutput) as UsageProbeResult;
|
||||
expect(successResult.first).toEqual({
|
||||
sessionUsage: 42,
|
||||
sessionResetAt: '2030-01-01T00:00:00.000Z',
|
||||
@@ -211,26 +318,281 @@ process.stdout.write(JSON.stringify({
|
||||
expect(successResult.second).toEqual(successResult.first);
|
||||
expect(successResult.cacheExists).toBe(true);
|
||||
expect(successResult.requestCount).toBe(1);
|
||||
expect(successResult.proxyAgentConfigured).toBe(false);
|
||||
expect(successResult.requestHost).toBe('api.anthropic.com');
|
||||
|
||||
const cachedSuccessOutput = execFileSync(process.execPath, [probeScriptPath], {
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
HOME: successHome,
|
||||
PATH: successBin,
|
||||
CLAUDE_CONFIG_DIR: successClaudeConfig,
|
||||
TEST_REQUEST_MODE: 'unexpected',
|
||||
TEST_NOW_MS: '2200000000000'
|
||||
}
|
||||
const httpsProxyResult = harness.runProbe({
|
||||
claudeConfigDir: proxyHome.claudeConfig,
|
||||
home: proxyHome.home,
|
||||
httpsProxy: 'http://proxy.local:8080',
|
||||
mode: 'success',
|
||||
nowMs,
|
||||
pathDir: proxyHome.bin,
|
||||
responseBody: successResponseBody
|
||||
});
|
||||
|
||||
expect(httpsProxyResult.first).toEqual(successResult.first);
|
||||
expect(httpsProxyResult.second).toEqual(successResult.first);
|
||||
expect(httpsProxyResult.requestCount).toBe(1);
|
||||
expect(httpsProxyResult.proxyAgentConfigured).toBe(true);
|
||||
expect(httpsProxyResult.requestHost).toBe('api.anthropic.com');
|
||||
|
||||
const lowercaseProxyResult = harness.runProbe({
|
||||
claudeConfigDir: lowercaseProxyHome.claudeConfig,
|
||||
home: lowercaseProxyHome.home,
|
||||
lowercaseHttpsProxy: 'http://proxy.local:8080',
|
||||
mode: 'success',
|
||||
nowMs,
|
||||
pathDir: lowercaseProxyHome.bin,
|
||||
responseBody: successResponseBody
|
||||
});
|
||||
|
||||
expect(lowercaseProxyResult.first).toEqual(successResult.first);
|
||||
expect(lowercaseProxyResult.second).toEqual(successResult.first);
|
||||
expect(lowercaseProxyResult.requestCount).toBe(1);
|
||||
expect(lowercaseProxyResult.proxyAgentConfigured).toBe(false);
|
||||
|
||||
const blankProxyResult = harness.runProbe({
|
||||
claudeConfigDir: blankProxyHome.claudeConfig,
|
||||
home: blankProxyHome.home,
|
||||
httpsProxy: ' ',
|
||||
mode: 'success',
|
||||
nowMs,
|
||||
pathDir: blankProxyHome.bin,
|
||||
responseBody: successResponseBody
|
||||
});
|
||||
|
||||
expect(blankProxyResult.first).toEqual(successResult.first);
|
||||
expect(blankProxyResult.second).toEqual(successResult.first);
|
||||
expect(blankProxyResult.requestCount).toBe(1);
|
||||
expect(blankProxyResult.proxyAgentConfigured).toBe(false);
|
||||
|
||||
const invalidProxyResult = harness.runProbe({
|
||||
claudeConfigDir: invalidProxyHome.claudeConfig,
|
||||
home: invalidProxyHome.home,
|
||||
httpsProxy: '://bad-proxy',
|
||||
mode: 'success',
|
||||
nowMs,
|
||||
pathDir: invalidProxyHome.bin,
|
||||
responseBody: successResponseBody
|
||||
});
|
||||
|
||||
expect(invalidProxyResult.first).toEqual({ error: 'api-error' });
|
||||
expect(invalidProxyResult.second).toEqual({ error: 'api-error' });
|
||||
expect(invalidProxyResult.requestCount).toBe(0);
|
||||
expect(invalidProxyResult.proxyAgentConfigured).toBe(false);
|
||||
|
||||
const staleProxyResult = harness.runProbe({
|
||||
claudeConfigDir: successHome.claudeConfig,
|
||||
home: successHome.home,
|
||||
httpsProxy: '://bad-proxy',
|
||||
mode: 'success',
|
||||
nowMs: nowMs + 181000,
|
||||
pathDir: successHome.bin,
|
||||
responseBody: successResponseBody
|
||||
});
|
||||
|
||||
expect(staleProxyResult.first).toEqual(successResult.first);
|
||||
expect(staleProxyResult.second).toEqual(successResult.first);
|
||||
expect(staleProxyResult.requestCount).toBe(0);
|
||||
expect(staleProxyResult.proxyAgentConfigured).toBe(false);
|
||||
|
||||
const cachedSuccessResult = harness.runProbe({
|
||||
claudeConfigDir: successHome.claudeConfig,
|
||||
home: successHome.home,
|
||||
mode: 'unexpected',
|
||||
nowMs,
|
||||
pathDir: successHome.bin
|
||||
});
|
||||
|
||||
const cachedSuccessResult = JSON.parse(cachedSuccessOutput) as UsageProbeResult;
|
||||
expect(cachedSuccessResult.first).toEqual(successResult.first);
|
||||
expect(cachedSuccessResult.second).toEqual(successResult.first);
|
||||
expect(cachedSuccessResult.cacheExists).toBe(true);
|
||||
expect(cachedSuccessResult.requestCount).toBe(1);
|
||||
expect(cachedSuccessResult.requestCount).toBe(0);
|
||||
} finally {
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||
harness.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
it('reuses stale cached data during a numeric Retry-After backoff and retries after expiry', () => {
|
||||
const harness = createProbeHarness();
|
||||
|
||||
try {
|
||||
const home = harness.createTokenHome('rate-limited-with-cache');
|
||||
const rateLimitNowMs = nowMs + 31000;
|
||||
const successResult = harness.runProbe({
|
||||
claudeConfigDir: home.claudeConfig,
|
||||
home: home.home,
|
||||
mode: 'success',
|
||||
nowMs,
|
||||
pathDir: home.bin,
|
||||
responseBody: successResponseBody
|
||||
});
|
||||
|
||||
const rateLimitedResult = harness.runProbe({
|
||||
claudeConfigDir: home.claudeConfig,
|
||||
home: home.home,
|
||||
mode: 'status',
|
||||
nowMs: rateLimitNowMs,
|
||||
pathDir: home.bin,
|
||||
responseBody: rateLimitedResponseBody,
|
||||
responseHeaders: { 'retry-after': '3600' },
|
||||
statusCode: 429
|
||||
});
|
||||
|
||||
expect(rateLimitedResult.first).toEqual(successResult.first);
|
||||
expect(rateLimitedResult.second).toEqual(successResult.first);
|
||||
expect(rateLimitedResult.requestCount).toBe(1);
|
||||
expect(parseLockContents(rateLimitedResult.lockContents)).toEqual({
|
||||
blockedUntil: Math.floor(rateLimitNowMs / 1000) + 3600,
|
||||
error: 'rate-limited'
|
||||
});
|
||||
|
||||
const activeBackoffResult = harness.runProbe({
|
||||
claudeConfigDir: home.claudeConfig,
|
||||
home: home.home,
|
||||
mode: 'unexpected',
|
||||
nowMs: rateLimitNowMs + 600000,
|
||||
pathDir: home.bin
|
||||
});
|
||||
|
||||
expect(activeBackoffResult.first).toEqual(successResult.first);
|
||||
expect(activeBackoffResult.second).toEqual(successResult.first);
|
||||
expect(activeBackoffResult.requestCount).toBe(0);
|
||||
|
||||
const postBackoffResult = harness.runProbe({
|
||||
claudeConfigDir: home.claudeConfig,
|
||||
home: home.home,
|
||||
mode: 'success',
|
||||
nowMs: rateLimitNowMs + 3601000,
|
||||
pathDir: home.bin,
|
||||
responseBody: updatedSuccessResponseBody
|
||||
});
|
||||
|
||||
expect(postBackoffResult.first).toEqual({
|
||||
sessionUsage: 55,
|
||||
sessionResetAt: '2030-01-02T00:00:00.000Z',
|
||||
weeklyUsage: 21,
|
||||
weeklyResetAt: '2030-01-08T00:00:00.000Z'
|
||||
});
|
||||
expect(postBackoffResult.second).toEqual(postBackoffResult.first);
|
||||
expect(postBackoffResult.requestCount).toBe(1);
|
||||
} finally {
|
||||
harness.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
it('returns rate-limited without stale cache and falls back to the default backoff when Retry-After is invalid', () => {
|
||||
const harness = createProbeHarness();
|
||||
|
||||
try {
|
||||
const home = harness.createTokenHome('rate-limited-no-cache');
|
||||
const firstRateLimitedResult = harness.runProbe({
|
||||
claudeConfigDir: home.claudeConfig,
|
||||
home: home.home,
|
||||
mode: 'status',
|
||||
nowMs,
|
||||
pathDir: home.bin,
|
||||
responseBody: rateLimitedResponseBody,
|
||||
responseHeaders: { 'retry-after': 'not-a-number' },
|
||||
statusCode: 429
|
||||
});
|
||||
|
||||
expect(firstRateLimitedResult.first).toEqual({ error: 'rate-limited' });
|
||||
expect(firstRateLimitedResult.second).toEqual({ error: 'rate-limited' });
|
||||
expect(firstRateLimitedResult.requestCount).toBe(1);
|
||||
expect(parseLockContents(firstRateLimitedResult.lockContents)).toEqual({
|
||||
blockedUntil: Math.floor(nowMs / 1000) + 300,
|
||||
error: 'rate-limited'
|
||||
});
|
||||
|
||||
const activeBackoffResult = harness.runProbe({
|
||||
claudeConfigDir: home.claudeConfig,
|
||||
home: home.home,
|
||||
mode: 'unexpected',
|
||||
nowMs: nowMs + 299000,
|
||||
pathDir: home.bin
|
||||
});
|
||||
|
||||
expect(activeBackoffResult.first).toEqual({ error: 'rate-limited' });
|
||||
expect(activeBackoffResult.second).toEqual({ error: 'rate-limited' });
|
||||
expect(activeBackoffResult.requestCount).toBe(0);
|
||||
|
||||
const postBackoffResult = harness.runProbe({
|
||||
claudeConfigDir: home.claudeConfig,
|
||||
home: home.home,
|
||||
mode: 'success',
|
||||
nowMs: nowMs + 301000,
|
||||
pathDir: home.bin,
|
||||
responseBody: successResponseBody
|
||||
});
|
||||
|
||||
expect(postBackoffResult.first).toEqual({
|
||||
sessionUsage: 42,
|
||||
sessionResetAt: '2030-01-01T00:00:00.000Z',
|
||||
weeklyUsage: 17,
|
||||
weeklyResetAt: '2030-01-07T00:00:00.000Z'
|
||||
});
|
||||
expect(postBackoffResult.second).toEqual(postBackoffResult.first);
|
||||
expect(postBackoffResult.requestCount).toBe(1);
|
||||
} finally {
|
||||
harness.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
it('parses HTTP-date Retry-After headers', () => {
|
||||
const harness = createProbeHarness();
|
||||
|
||||
try {
|
||||
const home = harness.createTokenHome('rate-limited-http-date');
|
||||
const retryAt = new Date(nowMs + 900000).toUTCString();
|
||||
const result = harness.runProbe({
|
||||
claudeConfigDir: home.claudeConfig,
|
||||
home: home.home,
|
||||
mode: 'status',
|
||||
nowMs,
|
||||
pathDir: home.bin,
|
||||
responseBody: rateLimitedResponseBody,
|
||||
responseHeaders: { 'retry-after': retryAt },
|
||||
statusCode: 429
|
||||
});
|
||||
|
||||
expect(result.first).toEqual({ error: 'rate-limited' });
|
||||
expect(result.second).toEqual({ error: 'rate-limited' });
|
||||
expect(parseLockContents(result.lockContents)).toEqual({
|
||||
blockedUntil: Math.floor((nowMs + 900000) / 1000),
|
||||
error: 'rate-limited'
|
||||
});
|
||||
} finally {
|
||||
harness.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
it('supports the legacy empty lock file fallback', () => {
|
||||
const harness = createProbeHarness();
|
||||
|
||||
try {
|
||||
const home = harness.createTokenHome('legacy-lock');
|
||||
const lockDir = path.join(home.home, '.cache', 'ccstatusline');
|
||||
const lockFile = path.join(lockDir, 'usage.lock');
|
||||
|
||||
fs.mkdirSync(lockDir, { recursive: true });
|
||||
fs.writeFileSync(lockFile, '');
|
||||
fs.utimesSync(lockFile, new Date(nowMs), new Date(nowMs));
|
||||
|
||||
const result = harness.runProbe({
|
||||
claudeConfigDir: home.claudeConfig,
|
||||
home: home.home,
|
||||
mode: 'unexpected',
|
||||
nowMs,
|
||||
pathDir: home.bin
|
||||
});
|
||||
|
||||
expect(result.first).toEqual({ error: 'timeout' });
|
||||
expect(result.second).toEqual({ error: 'timeout' });
|
||||
expect(result.requestCount).toBe(0);
|
||||
} finally {
|
||||
harness.cleanup();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import * as childProcess from 'child_process';
|
||||
import { createRequire } from 'module';
|
||||
import type { Mock } from 'vitest';
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi
|
||||
} from 'vitest';
|
||||
|
||||
import { getUsageToken } from '../usage-fetch';
|
||||
|
||||
vi.mock('child_process', () => ({ execFileSync: vi.fn() }));
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const { execFileSync: realExecFileSync } = require('node:child_process') as { execFileSync: typeof childProcess.execFileSync };
|
||||
const mockedExecFileSync = childProcess.execFileSync as Mock;
|
||||
|
||||
describe('getUsageToken dump-keychain behavior', () => {
|
||||
beforeEach(() => {
|
||||
mockedExecFileSync.mockReset();
|
||||
mockedExecFileSync.mockImplementation(realExecFileSync);
|
||||
vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
mockedExecFileSync.mockReset();
|
||||
mockedExecFileSync.mockImplementation(realExecFileSync);
|
||||
});
|
||||
|
||||
it('uses an expanded maxBuffer when dumping keychains for hashed fallbacks', () => {
|
||||
let dumpMaxBuffer: number | undefined;
|
||||
|
||||
mockedExecFileSync.mockImplementation((command: string, args?: string[], options?: { maxBuffer?: number }) => {
|
||||
if (command !== 'security') {
|
||||
return realExecFileSync(command, args, options);
|
||||
}
|
||||
|
||||
if (!args) {
|
||||
throw new Error('Expected security arguments');
|
||||
}
|
||||
|
||||
if (args[0] === 'find-generic-password' && args[2] === 'Claude Code-credentials') {
|
||||
throw new Error('missing exact credential');
|
||||
}
|
||||
|
||||
if (args[0] === 'dump-keychain') {
|
||||
dumpMaxBuffer = options?.maxBuffer;
|
||||
return [
|
||||
'keychain: "/Users/example/Library/Keychains/login.keychain-db"',
|
||||
'version: 512',
|
||||
'class: "genp"',
|
||||
'attributes:',
|
||||
' "svce"<blob>="Claude Code-credentials-hashed"',
|
||||
' "mdat"<timedate>="20240301010101Z"'
|
||||
].join('\n');
|
||||
}
|
||||
|
||||
if (args[0] === 'find-generic-password' && args[2] === 'Claude Code-credentials-hashed') {
|
||||
return JSON.stringify({ claudeAiOauth: { accessToken: 'hashed-token' } });
|
||||
}
|
||||
|
||||
throw new Error(`Unexpected security args: ${args.join(' ')}`);
|
||||
});
|
||||
|
||||
expect(getUsageToken()).toBe('hashed-token');
|
||||
expect(dumpMaxBuffer).toBe(8 * 1024 * 1024);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,330 @@
|
||||
import { execFileSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import {
|
||||
describe,
|
||||
expect,
|
||||
it
|
||||
} from 'vitest';
|
||||
|
||||
import { parseMacKeychainCredentialCandidates } from '../usage-fetch';
|
||||
|
||||
interface TokenHome {
|
||||
bin: string;
|
||||
claudeConfig: string;
|
||||
home: string;
|
||||
logFile: string;
|
||||
}
|
||||
|
||||
interface TokenProbeOptions {
|
||||
candidatePayloads?: Record<string, string>;
|
||||
dump?: string;
|
||||
dumpPaddingLines?: number;
|
||||
exactPayload?: string;
|
||||
platform: NodeJS.Platform;
|
||||
tokenHome: TokenHome;
|
||||
}
|
||||
|
||||
interface TokenProbeResult {
|
||||
first: string | null;
|
||||
second: string | null;
|
||||
securityLog: string[];
|
||||
}
|
||||
|
||||
function makeTokenPayload(token: string): string {
|
||||
return JSON.stringify({ claudeAiOauth: { accessToken: token } });
|
||||
}
|
||||
|
||||
function encodeAsciiAsHex(value: string): string {
|
||||
return Buffer.from(value, 'utf8').toString('hex');
|
||||
}
|
||||
|
||||
function makeKeychainBlock(service: string, modifiedAt?: { raw?: string; quoted?: string }): string {
|
||||
const lines = [
|
||||
'keychain: "/Users/example/Library/Keychains/login.keychain-db"',
|
||||
'version: 512',
|
||||
'class: "genp"',
|
||||
'attributes:',
|
||||
` "svce"<blob>="${service}"`
|
||||
];
|
||||
|
||||
if (modifiedAt?.raw && modifiedAt.quoted) {
|
||||
lines.push(` "mdat"<timedate>=0x${modifiedAt.raw} "${modifiedAt.quoted}"`);
|
||||
} else if (modifiedAt?.raw) {
|
||||
lines.push(` "mdat"<timedate>=0x${modifiedAt.raw}`);
|
||||
} else if (modifiedAt?.quoted) {
|
||||
lines.push(` "mdat"<timedate>="${modifiedAt.quoted}"`);
|
||||
}
|
||||
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function createTokenHarness() {
|
||||
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'ccstatusline-token-test-'));
|
||||
const probeScriptPath = path.join(tempRoot, 'probe-token.mjs');
|
||||
const usageFetchModulePath = fileURLToPath(new URL('../usage-fetch.ts', import.meta.url));
|
||||
|
||||
const probeScript = `
|
||||
import * as fs from 'fs';
|
||||
|
||||
if (process.env.TEST_PLATFORM) {
|
||||
Object.defineProperty(process, 'platform', {
|
||||
configurable: true,
|
||||
value: process.env.TEST_PLATFORM
|
||||
});
|
||||
}
|
||||
|
||||
const { getUsageToken } = await import(${JSON.stringify(usageFetchModulePath)});
|
||||
|
||||
const first = getUsageToken();
|
||||
const second = getUsageToken();
|
||||
const logFile = process.env.TEST_SECURITY_LOG_FILE;
|
||||
|
||||
process.stdout.write(JSON.stringify({
|
||||
first,
|
||||
second,
|
||||
securityLog: logFile && fs.existsSync(logFile)
|
||||
? fs.readFileSync(logFile, 'utf8').split(/\\r?\\n/).filter(Boolean)
|
||||
: []
|
||||
}));
|
||||
`;
|
||||
|
||||
const securityScript = `#!/usr/bin/env node
|
||||
const fs = require('fs');
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const logFile = process.env.TEST_SECURITY_LOG_FILE;
|
||||
if (logFile) {
|
||||
fs.appendFileSync(logFile, args.join(' ') + '\\n');
|
||||
}
|
||||
|
||||
if (args[0] === 'dump-keychain') {
|
||||
const paddingLines = Number.parseInt(process.env.TEST_SECURITY_DUMP_PADDING_LINES || '0', 10);
|
||||
let remainingPaddingLines = paddingLines;
|
||||
while (remainingPaddingLines > 0) {
|
||||
const chunkSize = Math.min(remainingPaddingLines, 1024);
|
||||
fs.writeSync(process.stdout.fd, 'ignored\\n'.repeat(chunkSize));
|
||||
remainingPaddingLines -= chunkSize;
|
||||
}
|
||||
fs.writeSync(process.stdout.fd, process.env.TEST_SECURITY_DUMP || '');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (args[0] !== 'find-generic-password') {
|
||||
process.exit(44);
|
||||
}
|
||||
|
||||
const serviceIndex = args.indexOf('-s');
|
||||
const service = serviceIndex >= 0 ? args[serviceIndex + 1] : '';
|
||||
const exactPayload = process.env.TEST_SECURITY_EXACT_PAYLOAD;
|
||||
const candidatePayloads = JSON.parse(process.env.TEST_SECURITY_CANDIDATE_PAYLOADS_JSON || '{}');
|
||||
|
||||
if (service === 'Claude Code-credentials') {
|
||||
if (exactPayload === undefined || exactPayload === '__MISSING__') {
|
||||
process.exit(44);
|
||||
}
|
||||
|
||||
fs.writeSync(process.stdout.fd, exactPayload);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (Object.prototype.hasOwnProperty.call(candidatePayloads, service)) {
|
||||
fs.writeSync(process.stdout.fd, candidatePayloads[service]);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
process.exit(44);
|
||||
`;
|
||||
|
||||
fs.writeFileSync(probeScriptPath, probeScript);
|
||||
|
||||
function createTokenHome(name: string, fileToken?: string): TokenHome {
|
||||
const home = path.join(tempRoot, `home-${name}`);
|
||||
const bin = path.join(tempRoot, `bin-${name}`);
|
||||
const claudeConfig = path.join(tempRoot, `claude-${name}`);
|
||||
const logFile = path.join(tempRoot, `security-${name}.log`);
|
||||
const securityPath = path.join(bin, 'security');
|
||||
|
||||
fs.mkdirSync(home, { recursive: true });
|
||||
fs.mkdirSync(bin, { recursive: true });
|
||||
fs.mkdirSync(claudeConfig, { recursive: true });
|
||||
fs.writeFileSync(securityPath, securityScript);
|
||||
fs.chmodSync(securityPath, 0o755);
|
||||
|
||||
if (fileToken) {
|
||||
fs.writeFileSync(
|
||||
path.join(claudeConfig, '.credentials.json'),
|
||||
makeTokenPayload(fileToken)
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
bin,
|
||||
claudeConfig,
|
||||
home,
|
||||
logFile
|
||||
};
|
||||
}
|
||||
|
||||
function runTokenProbe(options: TokenProbeOptions): TokenProbeResult {
|
||||
const output = execFileSync(process.execPath, [probeScriptPath], {
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
CLAUDE_CONFIG_DIR: options.tokenHome.claudeConfig,
|
||||
HOME: options.tokenHome.home,
|
||||
PATH: `${options.tokenHome.bin}${path.delimiter}${process.env.PATH ?? ''}`,
|
||||
TEST_PLATFORM: options.platform,
|
||||
TEST_SECURITY_CANDIDATE_PAYLOADS_JSON: JSON.stringify(options.candidatePayloads ?? {}),
|
||||
TEST_SECURITY_DUMP: options.dump ?? '',
|
||||
TEST_SECURITY_DUMP_PADDING_LINES: String(options.dumpPaddingLines ?? 0),
|
||||
TEST_SECURITY_EXACT_PAYLOAD: options.exactPayload ?? '__MISSING__',
|
||||
TEST_SECURITY_LOG_FILE: options.tokenHome.logFile
|
||||
}
|
||||
});
|
||||
|
||||
return JSON.parse(output) as TokenProbeResult;
|
||||
}
|
||||
|
||||
function cleanup(): void {
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
return {
|
||||
cleanup,
|
||||
createTokenHome,
|
||||
runTokenProbe
|
||||
};
|
||||
}
|
||||
|
||||
describe('parseMacKeychainCredentialCandidates', () => {
|
||||
it('returns hashed macOS credential candidates sorted newest-first and excludes the exact service', () => {
|
||||
const dump = [
|
||||
makeKeychainBlock('Claude Code-credentials', { quoted: '20240101010101Z' }),
|
||||
makeKeychainBlock('Claude Code-credentials-old', { quoted: '20240201010101Z' }),
|
||||
makeKeychainBlock('Claude Code-credentials-new', { quoted: '20240301010101Z' })
|
||||
].join('\n');
|
||||
|
||||
expect(parseMacKeychainCredentialCandidates(dump)).toEqual([
|
||||
'Claude Code-credentials-new',
|
||||
'Claude Code-credentials-old'
|
||||
]);
|
||||
});
|
||||
|
||||
it('uses discovered order when modified times are unavailable and parses hex-only timestamps when present', () => {
|
||||
const dump = [
|
||||
makeKeychainBlock('Claude Code-credentials-first'),
|
||||
makeKeychainBlock('Claude Code-credentials-second', { raw: encodeAsciiAsHex('20240401010101Z\0') }),
|
||||
makeKeychainBlock('Claude Code-credentials-third')
|
||||
].join('\n');
|
||||
|
||||
expect(parseMacKeychainCredentialCandidates(dump)).toEqual([
|
||||
'Claude Code-credentials-second',
|
||||
'Claude Code-credentials-first',
|
||||
'Claude Code-credentials-third'
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getUsageToken', () => {
|
||||
it('prefers the exact macOS keychain service over hashed fallbacks and files', () => {
|
||||
const harness = createTokenHarness();
|
||||
|
||||
try {
|
||||
const tokenHome = harness.createTokenHome('exact', 'file-token');
|
||||
const result = harness.runTokenProbe({
|
||||
exactPayload: makeTokenPayload('exact-token'),
|
||||
platform: 'darwin',
|
||||
tokenHome
|
||||
});
|
||||
|
||||
expect(result.first).toBe('exact-token');
|
||||
expect(result.second).toBe('exact-token');
|
||||
expect(result.securityLog).toEqual([
|
||||
'find-generic-password -s Claude Code-credentials -w',
|
||||
'find-generic-password -s Claude Code-credentials -w'
|
||||
]);
|
||||
} finally {
|
||||
harness.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
it('tries the newest hashed macOS keychain candidate after an exact miss', () => {
|
||||
const harness = createTokenHarness();
|
||||
|
||||
try {
|
||||
const tokenHome = harness.createTokenHome('hashed');
|
||||
const dump = [
|
||||
makeKeychainBlock('Claude Code-credentials-old', { quoted: '20240201010101Z' }),
|
||||
makeKeychainBlock('Claude Code-credentials-new', { quoted: '20240301010101Z' })
|
||||
].join('\n');
|
||||
const result = harness.runTokenProbe({
|
||||
candidatePayloads: { 'Claude Code-credentials-new': makeTokenPayload('hashed-token') },
|
||||
dump,
|
||||
platform: 'darwin',
|
||||
tokenHome
|
||||
});
|
||||
|
||||
expect(result.first).toBe('hashed-token');
|
||||
expect(result.second).toBe('hashed-token');
|
||||
expect(result.securityLog).toEqual([
|
||||
'find-generic-password -s Claude Code-credentials -w',
|
||||
'dump-keychain',
|
||||
'find-generic-password -s Claude Code-credentials-new -w',
|
||||
'find-generic-password -s Claude Code-credentials -w',
|
||||
'dump-keychain',
|
||||
'find-generic-password -s Claude Code-credentials-new -w'
|
||||
]);
|
||||
} finally {
|
||||
harness.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
it('falls back to ~/.claude/.credentials.json on macOS when keychain lookups miss or parse invalid data', () => {
|
||||
const harness = createTokenHarness();
|
||||
|
||||
try {
|
||||
const tokenHome = harness.createTokenHome('file-fallback', 'file-token');
|
||||
const dump = makeKeychainBlock('Claude Code-credentials-hashed', { quoted: '20240301010101Z' });
|
||||
const result = harness.runTokenProbe({
|
||||
candidatePayloads: { 'Claude Code-credentials-hashed': 'not-json' },
|
||||
dump,
|
||||
platform: 'darwin',
|
||||
tokenHome
|
||||
});
|
||||
|
||||
expect(result.first).toBe('file-token');
|
||||
expect(result.second).toBe('file-token');
|
||||
expect(result.securityLog).toEqual([
|
||||
'find-generic-password -s Claude Code-credentials -w',
|
||||
'dump-keychain',
|
||||
'find-generic-password -s Claude Code-credentials-hashed -w',
|
||||
'find-generic-password -s Claude Code-credentials -w',
|
||||
'dump-keychain',
|
||||
'find-generic-password -s Claude Code-credentials-hashed -w'
|
||||
]);
|
||||
} finally {
|
||||
harness.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
it('uses the credentials file on non-macOS', () => {
|
||||
const harness = createTokenHarness();
|
||||
|
||||
try {
|
||||
const tokenHome = harness.createTokenHome('linux', 'linux-file-token');
|
||||
const result = harness.runTokenProbe({
|
||||
platform: 'linux',
|
||||
tokenHome
|
||||
});
|
||||
|
||||
expect(result.first).toBe('linux-file-token');
|
||||
expect(result.second).toBe('linux-file-token');
|
||||
expect(result.securityLog).toEqual([]);
|
||||
} finally {
|
||||
harness.cleanup();
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
import {
|
||||
describe,
|
||||
expect,
|
||||
it
|
||||
} from 'vitest';
|
||||
|
||||
import { getUsageErrorMessage } from '../usage-windows';
|
||||
|
||||
describe('getUsageErrorMessage', () => {
|
||||
it('returns the rate-limited label', () => {
|
||||
expect(getUsageErrorMessage('rate-limited')).toBe('[Rate limited]');
|
||||
});
|
||||
});
|
||||
@@ -134,9 +134,40 @@ describe('usage window helpers', () => {
|
||||
});
|
||||
|
||||
it('formats duration in block timer style', () => {
|
||||
expect(formatUsageDuration(0)).toBe('0hr');
|
||||
expect(formatUsageDuration(0)).toBe('0m');
|
||||
expect(formatUsageDuration(3 * 60 * 60 * 1000)).toBe('3hr');
|
||||
expect(formatUsageDuration(3.5 * 60 * 60 * 1000)).toBe('3hr 30m');
|
||||
expect(formatUsageDuration(4 * 60 * 60 * 1000 + 5 * 60 * 1000)).toBe('4hr 5m');
|
||||
});
|
||||
|
||||
it('formats duration with days when >= 24h', () => {
|
||||
expect(formatUsageDuration(25 * 60 * 60 * 1000)).toBe('1d 1hr');
|
||||
expect(formatUsageDuration(36.5 * 60 * 60 * 1000)).toBe('1d 12hr 30m');
|
||||
expect(formatUsageDuration(168 * 60 * 60 * 1000)).toBe('7d');
|
||||
});
|
||||
|
||||
it('formats duration in compact style', () => {
|
||||
expect(formatUsageDuration(0, true)).toBe('0m');
|
||||
expect(formatUsageDuration(3 * 60 * 60 * 1000, true)).toBe('3h');
|
||||
expect(formatUsageDuration(3.5 * 60 * 60 * 1000, true)).toBe('3h30m');
|
||||
expect(formatUsageDuration(4 * 60 * 60 * 1000 + 5 * 60 * 1000, true)).toBe('4h5m');
|
||||
});
|
||||
|
||||
it('formats duration with days in compact style when >= 24h', () => {
|
||||
expect(formatUsageDuration(25 * 60 * 60 * 1000, true)).toBe('1d1h');
|
||||
expect(formatUsageDuration(36.5 * 60 * 60 * 1000, true)).toBe('1d12h30m');
|
||||
expect(formatUsageDuration(168 * 60 * 60 * 1000, true)).toBe('7d');
|
||||
});
|
||||
|
||||
it('formats duration without days when requested', () => {
|
||||
expect(formatUsageDuration(25 * 60 * 60 * 1000, false, false)).toBe('25hr');
|
||||
expect(formatUsageDuration(36.5 * 60 * 60 * 1000, false, false)).toBe('36hr 30m');
|
||||
expect(formatUsageDuration(168 * 60 * 60 * 1000, false, false)).toBe('168hr');
|
||||
});
|
||||
|
||||
it('formats duration without days in compact style when requested', () => {
|
||||
expect(formatUsageDuration(25 * 60 * 60 * 1000, true, false)).toBe('25h');
|
||||
expect(formatUsageDuration(36.5 * 60 * 60 * 1000, true, false)).toBe('36h30m');
|
||||
expect(formatUsageDuration(168 * 60 * 60 * 1000, true, false)).toBe('168h');
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,10 @@ import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
import type { ClaudeSettings } from '../types/ClaudeSettings';
|
||||
import {
|
||||
SettingsSchema,
|
||||
type Settings
|
||||
} from '../types/Settings';
|
||||
|
||||
import {
|
||||
getConfigPath,
|
||||
@@ -110,6 +114,26 @@ async function backupClaudeSettings(suffix = '.bak'): Promise<string | null> {
|
||||
|
||||
interface LoadClaudeSettingsOptions { logErrors?: boolean }
|
||||
|
||||
export function loadClaudeSettingsSync(options: LoadClaudeSettingsOptions = {}): ClaudeSettings {
|
||||
const { logErrors = true } = options;
|
||||
const settingsPath = getClaudeSettingsPath();
|
||||
|
||||
// File doesn't exist - return empty object
|
||||
if (!fs.existsSync(settingsPath)) {
|
||||
return {};
|
||||
}
|
||||
|
||||
try {
|
||||
const content = fs.readFileSync(settingsPath, 'utf-8');
|
||||
return JSON.parse(content) as ClaudeSettings;
|
||||
} catch (error) {
|
||||
if (logErrors) {
|
||||
console.error('Failed to load Claude settings:', error);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadClaudeSettings(options: LoadClaudeSettingsOptions = {}): Promise<ClaudeSettings> {
|
||||
const { logErrors = true } = options;
|
||||
const settingsPath = getClaudeSettingsPath();
|
||||
@@ -178,6 +202,25 @@ function buildCommand(baseCommand: string): string {
|
||||
return baseCommand;
|
||||
}
|
||||
|
||||
async function loadSavedSettingsForHookSync(): Promise<Settings | null> {
|
||||
const configPath = getConfigPath();
|
||||
if (!fs.existsSync(configPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const content = await readFile(configPath, 'utf-8');
|
||||
const parsed = JSON.parse(content) as unknown;
|
||||
const result = SettingsSchema.safeParse(parsed);
|
||||
if (!result.success) {
|
||||
return null;
|
||||
}
|
||||
return result.data;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function installStatusLine(useBunx = false): Promise<void> {
|
||||
let settings: ClaudeSettings;
|
||||
|
||||
@@ -202,6 +245,12 @@ export async function installStatusLine(useBunx = false): Promise<void> {
|
||||
};
|
||||
|
||||
await saveClaudeSettings(settings);
|
||||
|
||||
const savedSettings = await loadSavedSettingsForHookSync();
|
||||
if (savedSettings) {
|
||||
const { syncWidgetHooks } = await import('./hooks');
|
||||
await syncWidgetHooks(savedSettings);
|
||||
}
|
||||
}
|
||||
|
||||
export async function uninstallStatusLine(): Promise<void> {
|
||||
@@ -218,6 +267,13 @@ export async function uninstallStatusLine(): Promise<void> {
|
||||
delete settings.statusLine;
|
||||
await saveClaudeSettings(settings);
|
||||
}
|
||||
|
||||
try {
|
||||
const { removeManagedHooks } = await import('./hooks');
|
||||
await removeManagedHooks();
|
||||
} catch {
|
||||
// Ignore hook cleanup failures during uninstall
|
||||
}
|
||||
}
|
||||
|
||||
export async function getExistingStatusLine(): Promise<string | null> {
|
||||
|
||||
@@ -158,4 +158,10 @@ export async function saveSettings(settings: Settings): Promise<void> {
|
||||
};
|
||||
|
||||
await writeSettingsJson(settingsWithVersion, paths);
|
||||
|
||||
// Sync widget hooks to Claude settings
|
||||
try {
|
||||
const { syncWidgetHooks } = await import('./hooks');
|
||||
await syncWidgetHooks(settings);
|
||||
} catch { /* ignore hook sync failures */ }
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import type { Settings } from '../types/Settings';
|
||||
import type { Widget } from '../types/Widget';
|
||||
|
||||
import {
|
||||
getExistingStatusLine,
|
||||
loadClaudeSettings,
|
||||
saveClaudeSettings
|
||||
} from './claude-settings';
|
||||
import { getWidget } from './widgets';
|
||||
|
||||
export interface WidgetHookDef {
|
||||
event: string;
|
||||
matcher?: string;
|
||||
}
|
||||
|
||||
const HOOK_TAG = 'ccstatusline-managed';
|
||||
|
||||
interface HookEntry {
|
||||
_tag?: string;
|
||||
matcher?: string;
|
||||
hooks?: { type: string; command: string }[];
|
||||
}
|
||||
|
||||
function stripManagedHooks(hooks: Record<string, HookEntry[]>): void {
|
||||
for (const event of Object.keys(hooks)) {
|
||||
hooks[event] = (hooks[event] ?? []).filter(entry => entry._tag !== HOOK_TAG);
|
||||
if (hooks[event].length === 0) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
|
||||
delete hooks[event];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getActiveHookDefs(settings: Settings): WidgetHookDef[] {
|
||||
const seen = new Set<string>();
|
||||
const defs: WidgetHookDef[] = [];
|
||||
for (const line of settings.lines) {
|
||||
for (const item of line) {
|
||||
const widget = getWidget(item.type) as (Widget & { getHooks?: () => WidgetHookDef[] }) | null;
|
||||
if (!widget?.getHooks) {
|
||||
continue;
|
||||
}
|
||||
for (const hook of widget.getHooks()) {
|
||||
const key = `${hook.event}:${hook.matcher ?? ''}`;
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
defs.push(hook);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return defs;
|
||||
}
|
||||
|
||||
export async function syncWidgetHooks(settings: Settings): Promise<void> {
|
||||
const needed = getActiveHookDefs(settings);
|
||||
const claudeSettings = await loadClaudeSettings({ logErrors: false });
|
||||
const hooks = (claudeSettings.hooks ?? {}) as Record<string, HookEntry[]>;
|
||||
|
||||
// Remove all ccstatusline-managed hooks
|
||||
stripManagedHooks(hooks);
|
||||
|
||||
const statusCommand = await getExistingStatusLine();
|
||||
if (!statusCommand) {
|
||||
claudeSettings.hooks = Object.keys(hooks).length > 0 ? hooks : undefined;
|
||||
await saveClaudeSettings(claudeSettings);
|
||||
return;
|
||||
}
|
||||
const hookCommand = `${statusCommand} --hook`;
|
||||
|
||||
// Add needed hooks
|
||||
for (const def of needed) {
|
||||
const entry: HookEntry = {
|
||||
_tag: HOOK_TAG,
|
||||
hooks: [{ type: 'command', command: hookCommand }]
|
||||
};
|
||||
if (def.matcher) {
|
||||
entry.matcher = def.matcher;
|
||||
}
|
||||
const list = hooks[def.event] ??= [];
|
||||
list.push(entry);
|
||||
}
|
||||
|
||||
claudeSettings.hooks = Object.keys(hooks).length > 0 ? hooks : undefined;
|
||||
await saveClaudeSettings(claudeSettings);
|
||||
}
|
||||
|
||||
export async function removeManagedHooks(): Promise<void> {
|
||||
const claudeSettings = await loadClaudeSettings({ logErrors: false });
|
||||
const hooks = (claudeSettings.hooks ?? {}) as Record<string, HookEntry[]>;
|
||||
|
||||
stripManagedHooks(hooks);
|
||||
|
||||
claudeSettings.hooks = Object.keys(hooks).length > 0 ? hooks : undefined;
|
||||
await saveClaudeSettings(claudeSettings);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
export const IDE_LINK_MODES = [
|
||||
'vscode',
|
||||
'cursor'
|
||||
] as const;
|
||||
|
||||
export type IdeLinkMode = (typeof IDE_LINK_MODES)[number];
|
||||
|
||||
export function renderOsc8Link(url: string, text: string): string {
|
||||
return `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`;
|
||||
}
|
||||
|
||||
function parseGitHubRepositoryPath(pathname: string): string | null {
|
||||
const trimmedPath = pathname.replace(/^\/+|\/+$/g, '').replace(/\.git$/, '');
|
||||
const segments = trimmedPath.split('/').filter(Boolean);
|
||||
|
||||
if (segments.length !== 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `${segments[0]}/${segments[1]}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a git remote URL to a GitHub HTTPS base URL.
|
||||
* Handles SSH, HTTPS, and ssh:// URL formats.
|
||||
* Returns null if the remote is not a GitHub URL.
|
||||
*/
|
||||
export function parseGitHubBaseUrl(remoteUrl: string): string | null {
|
||||
const trimmed = remoteUrl.trim();
|
||||
if (trimmed.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const sshMatch = /^(?:[^@]+@)?github\.com:([^/]+\/[^/]+?)(?:\.git)?\/?$/.exec(trimmed);
|
||||
if (sshMatch?.[1]) {
|
||||
return `https://github.com/${sshMatch[1]}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedUrl = new URL(trimmed);
|
||||
const supportedProtocols = new Set([
|
||||
'http:',
|
||||
'https:',
|
||||
'ssh:',
|
||||
'git:'
|
||||
]);
|
||||
|
||||
if (parsedUrl.hostname.toLowerCase() !== 'github.com' || !supportedProtocols.has(parsedUrl.protocol)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const repoPath = parseGitHubRepositoryPath(parsedUrl.pathname);
|
||||
if (!repoPath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return `https://github.com/${repoPath}`;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function encodeGitRefForUrlPath(ref: string): string {
|
||||
return ref
|
||||
.split('/')
|
||||
.map(segment => encodeURIComponent(segment))
|
||||
.join('/');
|
||||
}
|
||||
|
||||
function encodeFilePathForUri(path: string): string {
|
||||
return path
|
||||
.replace(/\\/g, '/')
|
||||
.split('/')
|
||||
.map(segment => encodeURIComponent(segment))
|
||||
.join('/');
|
||||
}
|
||||
|
||||
export function buildIdeFileUrl(filePath: string, ideLinkMode: IdeLinkMode): string {
|
||||
const normalizedPath = filePath.replace(/\\/g, '/');
|
||||
const uncMatch = /^\/\/([^/]+)(\/.*)?$/.exec(normalizedPath);
|
||||
if (uncMatch?.[1]) {
|
||||
const encodedPath = encodeFilePathForUri(uncMatch[2] ?? '/');
|
||||
return `${ideLinkMode}://file//${uncMatch[1]}${encodedPath}`;
|
||||
}
|
||||
|
||||
const driveMatch = /^([A-Za-z]:)(\/.*)?$/.exec(normalizedPath);
|
||||
if (driveMatch?.[1]) {
|
||||
const encodedPath = encodeFilePathForUri(driveMatch[2] ?? '/');
|
||||
return `${ideLinkMode}://file/${driveMatch[1]}${encodedPath}`;
|
||||
}
|
||||
|
||||
return `${ideLinkMode}://file${encodeFilePathForUri(normalizedPath)}`;
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { getVisibleText } from './ansi';
|
||||
import {
|
||||
parseJsonlLine,
|
||||
readJsonlLinesSync
|
||||
} from './jsonl-lines';
|
||||
|
||||
export type TranscriptThinkingEffort = 'low' | 'medium' | 'high' | 'max';
|
||||
|
||||
const MODEL_STDOUT_PREFIX = '<local-command-stdout>Set model to ';
|
||||
const MODEL_STDOUT_EFFORT_REGEX = /^<local-command-stdout>Set model to[\s\S]*? with (low|medium|high|max) effort<\/local-command-stdout>$/i;
|
||||
|
||||
interface TranscriptEntry { message?: { content?: string } }
|
||||
|
||||
function normalizeThinkingEffort(value: string | undefined): TranscriptThinkingEffort | undefined {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const normalized = value.toLowerCase();
|
||||
if (normalized === 'low' || normalized === 'medium' || normalized === 'high' || normalized === 'max') {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function getTranscriptThinkingEffort(transcriptPath: string | undefined): TranscriptThinkingEffort | undefined {
|
||||
if (!transcriptPath) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
try {
|
||||
const lines = readJsonlLinesSync(transcriptPath);
|
||||
|
||||
for (let i = lines.length - 1; i >= 0; i--) {
|
||||
const line = lines[i];
|
||||
if (!line) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const entry = parseJsonlLine(line) as TranscriptEntry | null;
|
||||
if (typeof entry?.message?.content !== 'string') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const visibleContent = getVisibleText(entry.message.content).trim();
|
||||
if (!visibleContent.startsWith(MODEL_STDOUT_PREFIX)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const match = MODEL_STDOUT_EFFORT_REGEX.exec(visibleContent);
|
||||
return normalizeThinkingEffort(match?.[1]);
|
||||
}
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
+2
-1
@@ -10,4 +10,5 @@ export {
|
||||
getSpeedMetrics,
|
||||
getSpeedMetricsCollection,
|
||||
getTokenMetrics
|
||||
} from './jsonl-metrics';
|
||||
} from './jsonl-metrics';
|
||||
export { getTranscriptThinkingEffort } from './jsonl-metadata';
|
||||
@@ -155,10 +155,10 @@ function renderPowerlineStatusLine(
|
||||
// Use pre-rendered content - use the correct index from the mapping
|
||||
const actualPreRenderedIndex = preRenderedIndices[i];
|
||||
const preRendered = actualPreRenderedIndex !== undefined ? preRenderedWidgets[actualPreRenderedIndex] : undefined;
|
||||
const widgetImpl = getWidget(widget.type);
|
||||
if (preRendered?.content) {
|
||||
widgetText = preRendered.content;
|
||||
// Get default color from widget impl for consistency
|
||||
const widgetImpl = getWidget(widget.type);
|
||||
if (widgetImpl) {
|
||||
defaultColor = widgetImpl.getDefaultColor();
|
||||
}
|
||||
@@ -352,7 +352,7 @@ function renderPowerlineStatusLine(
|
||||
// - Background: previous widget's background color
|
||||
|
||||
// Build separator with raw ANSI codes to avoid reset issues
|
||||
let separatorOutput = '';
|
||||
let separatorOutput: string;
|
||||
|
||||
// Check if adjacent widgets have the same background color
|
||||
const sameBackground = widget.bgColor && nextWidget.bgColor && widget.bgColor === nextWidget.bgColor;
|
||||
@@ -832,7 +832,7 @@ export function renderStatusLine(
|
||||
});
|
||||
|
||||
// Build the final status line
|
||||
let statusLine = '';
|
||||
let statusLine: string;
|
||||
|
||||
if (hasFlexSeparator && terminalWidth) {
|
||||
// Split elements by flex separators
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
import type {
|
||||
SkillInvocation,
|
||||
SkillsMetrics
|
||||
} from '../types/SkillsMetrics';
|
||||
|
||||
const EMPTY: SkillsMetrics = { totalInvocations: 0, uniqueSkills: [], lastSkill: null };
|
||||
|
||||
function getSkillsDir(): string {
|
||||
return path.join(os.homedir(), '.cache', 'ccstatusline', 'skills');
|
||||
}
|
||||
|
||||
export function getSkillsFilePath(sessionId: string): string {
|
||||
return path.join(getSkillsDir(), `skills-${sessionId}.jsonl`);
|
||||
}
|
||||
|
||||
export function getSkillsMetrics(sessionId: string): SkillsMetrics {
|
||||
const filePath = getSkillsFilePath(sessionId);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
try {
|
||||
const invocations: SkillInvocation[] = fs.readFileSync(filePath, 'utf-8')
|
||||
.trim().split('\n')
|
||||
.filter(line => line.trim())
|
||||
.map((line) => {
|
||||
try { return JSON.parse(line) as SkillInvocation; } catch {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((e): e is SkillInvocation => e !== null && typeof e.skill === 'string' && typeof e.session_id === 'string');
|
||||
if (invocations.length === 0) {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
const uniqueSkills: string[] = [];
|
||||
const seenSkills = new Set<string>();
|
||||
for (let i = invocations.length - 1; i >= 0; i--) {
|
||||
const skill = invocations[i]?.skill;
|
||||
if (skill && !seenSkills.has(skill)) {
|
||||
seenSkills.add(skill);
|
||||
uniqueSkills.push(skill);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalInvocations: invocations.length,
|
||||
uniqueSkills,
|
||||
lastSkill: invocations[invocations.length - 1]?.skill ?? null
|
||||
};
|
||||
} catch {
|
||||
return EMPTY;
|
||||
}
|
||||
}
|
||||
+328
-91
@@ -1,6 +1,7 @@
|
||||
import { execSync } from 'child_process';
|
||||
import { execFileSync } from 'child_process';
|
||||
import * as fs from 'fs';
|
||||
import * as https from 'https';
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { z } from 'zod';
|
||||
@@ -18,9 +19,16 @@ const CACHE_FILE = path.join(CACHE_DIR, 'usage.json');
|
||||
const LOCK_FILE = path.join(CACHE_DIR, 'usage.lock');
|
||||
const CACHE_MAX_AGE = 180; // seconds
|
||||
const LOCK_MAX_AGE = 30; // rate limit: only try API once per 30 seconds
|
||||
const TOKEN_CACHE_MAX_AGE = 3600; // 1 hour
|
||||
const DEFAULT_RATE_LIMIT_BACKOFF = 300; // seconds
|
||||
const MACOS_USAGE_CREDENTIALS_SERVICE = 'Claude Code-credentials';
|
||||
const MACOS_SECURITY_DUMP_MAX_BUFFER = 8 * 1024 * 1024;
|
||||
|
||||
const UsageCredentialsSchema = z.object({ claudeAiOauth: z.object({ accessToken: z.string().nullable().optional() }).optional() });
|
||||
const UsageLockErrorSchema = z.enum(['timeout', 'rate-limited']);
|
||||
const UsageLockSchema = z.object({
|
||||
blockedUntil: z.number(),
|
||||
error: UsageLockErrorSchema.optional()
|
||||
});
|
||||
|
||||
const CachedUsageDataSchema = z.object({
|
||||
sessionUsage: z.number().nullable().optional(),
|
||||
@@ -107,63 +115,202 @@ function parseUsageApiResponse(rawJson: string): UsageData | null {
|
||||
// Memory caches
|
||||
let cachedUsageData: UsageData | null = null;
|
||||
let usageCacheTime = 0;
|
||||
let cachedUsageToken: string | null = null;
|
||||
let usageTokenCacheTime = 0;
|
||||
let usageErrorCacheMaxAge = LOCK_MAX_AGE;
|
||||
|
||||
function setCachedUsageError(error: UsageError, now: number): UsageData {
|
||||
type UsageLockError = z.infer<typeof UsageLockErrorSchema>;
|
||||
|
||||
type UsageApiFetchResult = { kind: 'success'; body: string } | { kind: 'rate-limited'; retryAfterSeconds: number } | { kind: 'error' };
|
||||
interface MacKeychainCredentialCandidate {
|
||||
modifiedAt: string | null;
|
||||
order: number;
|
||||
service: string;
|
||||
}
|
||||
|
||||
function ensureCacheDirExists(): void {
|
||||
if (!fs.existsSync(CACHE_DIR)) {
|
||||
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
function setCachedUsageError(error: UsageError, now: number, maxAge = LOCK_MAX_AGE): UsageData {
|
||||
const errorData: UsageData = { error };
|
||||
cachedUsageData = errorData;
|
||||
usageCacheTime = now;
|
||||
usageErrorCacheMaxAge = maxAge;
|
||||
return errorData;
|
||||
}
|
||||
|
||||
function getStaleUsageOrError(error: UsageError, now: number): UsageData {
|
||||
const stale = readStaleUsageCache();
|
||||
if (stale && !stale.error) {
|
||||
cachedUsageData = stale;
|
||||
usageCacheTime = now;
|
||||
return stale;
|
||||
}
|
||||
return setCachedUsageError(error, now);
|
||||
function cacheUsageData(data: UsageData, now: number): UsageData {
|
||||
cachedUsageData = data;
|
||||
usageCacheTime = now;
|
||||
usageErrorCacheMaxAge = LOCK_MAX_AGE;
|
||||
return data;
|
||||
}
|
||||
|
||||
function getUsageToken(): string | null {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
function getStaleUsageOrError(error: UsageError, now: number, errorCacheMaxAge = LOCK_MAX_AGE): UsageData {
|
||||
const stale = readStaleUsageCache();
|
||||
if (stale && !stale.error) {
|
||||
return cacheUsageData(stale, now);
|
||||
}
|
||||
return setCachedUsageError(error, now, errorCacheMaxAge);
|
||||
}
|
||||
|
||||
// Return cached token if still valid
|
||||
if (cachedUsageToken && (now - usageTokenCacheTime) < TOKEN_CACHE_MAX_AGE) {
|
||||
return cachedUsageToken;
|
||||
function normalizeSecurityTimedateValue(rawValue: string): string | null {
|
||||
const cleaned = rawValue.replace(/\\000/g, '').replace(/\0/g, '').trim();
|
||||
return /^\d{14}Z$/.test(cleaned) ? cleaned : null;
|
||||
}
|
||||
|
||||
function decodeHexAscii(rawHex: string): string | null {
|
||||
if (rawHex.length === 0 || rawHex.length % 2 !== 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const isMac = process.platform === 'darwin';
|
||||
if (isMac) {
|
||||
// macOS: read from keychain
|
||||
const result = execSync(
|
||||
'security find-generic-password -s "Claude Code-credentials" -w 2>/dev/null',
|
||||
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }
|
||||
).trim();
|
||||
const token = parseUsageAccessToken(result);
|
||||
if (token) {
|
||||
cachedUsageToken = token;
|
||||
usageTokenCacheTime = now;
|
||||
}
|
||||
return token;
|
||||
let decoded = '';
|
||||
|
||||
for (let i = 0; i < rawHex.length; i += 2) {
|
||||
const byte = Number.parseInt(rawHex.slice(i, i + 2), 16);
|
||||
if (Number.isNaN(byte)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Non-macOS: read from credentials file, honoring CLAUDE_CONFIG_DIR
|
||||
const credFile = path.join(getClaudeConfigDir(), '.credentials.json');
|
||||
const token = parseUsageAccessToken(fs.readFileSync(credFile, 'utf8'));
|
||||
if (token) {
|
||||
cachedUsageToken = token;
|
||||
usageTokenCacheTime = now;
|
||||
decoded += String.fromCharCode(byte);
|
||||
}
|
||||
|
||||
return decoded;
|
||||
}
|
||||
|
||||
function parseModifiedTimeFromKeychainBlock(block: string): string | null {
|
||||
const quotedMatch = /"mdat"<timedate>=(?:0x[0-9A-Fa-f]+\s+)?"([^"]+)"/.exec(block);
|
||||
if (quotedMatch?.[1]) {
|
||||
const parsed = normalizeSecurityTimedateValue(quotedMatch[1]);
|
||||
if (parsed !== null) {
|
||||
return parsed;
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
const hexMatch = /"mdat"<timedate>=0x([0-9A-Fa-f]+)/.exec(block);
|
||||
if (!hexMatch?.[1]) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const decoded = decodeHexAscii(hexMatch[1]);
|
||||
return decoded ? normalizeSecurityTimedateValue(decoded) : null;
|
||||
}
|
||||
|
||||
function sortMacKeychainCredentialCandidates(a: MacKeychainCredentialCandidate, b: MacKeychainCredentialCandidate): number {
|
||||
if (a.modifiedAt !== null && b.modifiedAt !== null && a.modifiedAt !== b.modifiedAt) {
|
||||
return b.modifiedAt.localeCompare(a.modifiedAt);
|
||||
}
|
||||
|
||||
if (a.modifiedAt !== null && b.modifiedAt === null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (a.modifiedAt === null && b.modifiedAt !== null) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return a.order - b.order;
|
||||
}
|
||||
|
||||
export function parseMacKeychainCredentialCandidates(rawDump: string, servicePrefix = MACOS_USAGE_CREDENTIALS_SERVICE): string[] {
|
||||
const blocks = rawDump.split(/(?=^keychain:\s)/m).filter(block => block.trim().length > 0);
|
||||
const dedupedCandidates = new Map<string, MacKeychainCredentialCandidate>();
|
||||
let order = 0;
|
||||
|
||||
for (const block of blocks) {
|
||||
const serviceMatch = /"svce"<blob>="([^"]+)"/.exec(block);
|
||||
const service = serviceMatch?.[1];
|
||||
|
||||
if (!service || !service.startsWith(servicePrefix) || service === MACOS_USAGE_CREDENTIALS_SERVICE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const candidate: MacKeychainCredentialCandidate = {
|
||||
modifiedAt: parseModifiedTimeFromKeychainBlock(block),
|
||||
order,
|
||||
service
|
||||
};
|
||||
order += 1;
|
||||
|
||||
const existing = dedupedCandidates.get(service);
|
||||
if (!existing || sortMacKeychainCredentialCandidates(candidate, existing) < 0) {
|
||||
dedupedCandidates.set(service, candidate);
|
||||
}
|
||||
}
|
||||
|
||||
return [...dedupedCandidates.values()]
|
||||
.sort(sortMacKeychainCredentialCandidates)
|
||||
.map(candidate => candidate.service);
|
||||
}
|
||||
|
||||
function readMacKeychainSecret(service: string): string | null {
|
||||
try {
|
||||
return execFileSync(
|
||||
'security',
|
||||
['find-generic-password', '-s', service, '-w'],
|
||||
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }
|
||||
).trim();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function readUsageTokenFromMacKeychainService(service: string): string | null {
|
||||
const secret = readMacKeychainSecret(service);
|
||||
return secret ? parseUsageAccessToken(secret) : null;
|
||||
}
|
||||
|
||||
function listMacKeychainCredentialCandidates(): string[] {
|
||||
try {
|
||||
const rawDump = execFileSync(
|
||||
'security',
|
||||
['dump-keychain'],
|
||||
{
|
||||
encoding: 'utf8',
|
||||
maxBuffer: MACOS_SECURITY_DUMP_MAX_BUFFER,
|
||||
stdio: ['pipe', 'pipe', 'ignore']
|
||||
}
|
||||
);
|
||||
|
||||
return parseMacKeychainCredentialCandidates(rawDump);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function readUsageTokenFromMacKeychainCandidates(): string | null {
|
||||
const candidates = listMacKeychainCredentialCandidates();
|
||||
|
||||
for (const service of candidates) {
|
||||
const token = readUsageTokenFromMacKeychainService(service);
|
||||
if (token) {
|
||||
return token;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function readUsageTokenFromCredentialsFile(): string | null {
|
||||
try {
|
||||
const credFile = path.join(getClaudeConfigDir(), '.credentials.json');
|
||||
return parseUsageAccessToken(fs.readFileSync(credFile, 'utf8'));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function getUsageToken(): string | null {
|
||||
if (process.platform !== 'darwin') {
|
||||
return readUsageTokenFromCredentialsFile();
|
||||
}
|
||||
|
||||
return readUsageTokenFromMacKeychainService(MACOS_USAGE_CREDENTIALS_SERVICE)
|
||||
?? readUsageTokenFromMacKeychainCandidates()
|
||||
?? readUsageTokenFromCredentialsFile();
|
||||
}
|
||||
|
||||
function readStaleUsageCache(): UsageData | null {
|
||||
try {
|
||||
return parseCachedUsageData(fs.readFileSync(CACHE_FILE, 'utf8'));
|
||||
@@ -172,22 +319,94 @@ function readStaleUsageCache(): UsageData | null {
|
||||
}
|
||||
}
|
||||
|
||||
function writeUsageLock(blockedUntil: number, error: UsageLockError): void {
|
||||
try {
|
||||
ensureCacheDirExists();
|
||||
fs.writeFileSync(LOCK_FILE, JSON.stringify({ blockedUntil, error }));
|
||||
} catch {
|
||||
// Ignore lock file errors
|
||||
}
|
||||
}
|
||||
|
||||
function readActiveUsageLock(now: number): { blockedUntil: number; error: UsageLockError } | null {
|
||||
let hasValidJsonLock = false;
|
||||
|
||||
try {
|
||||
const parsed = parseJsonWithSchema(fs.readFileSync(LOCK_FILE, 'utf8'), UsageLockSchema);
|
||||
if (parsed) {
|
||||
hasValidJsonLock = true;
|
||||
if (parsed.blockedUntil > now) {
|
||||
return {
|
||||
blockedUntil: parsed.blockedUntil,
|
||||
error: parsed.error ?? 'timeout'
|
||||
};
|
||||
}
|
||||
return null;
|
||||
}
|
||||
} catch {
|
||||
// Fall back to the legacy mtime-based lock behavior below.
|
||||
}
|
||||
|
||||
if (hasValidJsonLock) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const lockStat = fs.statSync(LOCK_FILE);
|
||||
const lockMtime = Math.floor(lockStat.mtimeMs / 1000);
|
||||
const blockedUntil = lockMtime + LOCK_MAX_AGE;
|
||||
if (blockedUntil > now) {
|
||||
return {
|
||||
blockedUntil,
|
||||
error: 'timeout'
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
// Lock file doesn't exist - OK to proceed
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function parseRetryAfterSeconds(headerValue: string | string[] | undefined, nowMs = Date.now()): number | null {
|
||||
const rawValue = Array.isArray(headerValue) ? headerValue[0] : headerValue;
|
||||
const trimmedValue = rawValue?.trim();
|
||||
if (!trimmedValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (/^\d+$/.test(trimmedValue)) {
|
||||
const seconds = Number.parseInt(trimmedValue, 10);
|
||||
return seconds > 0 ? seconds : null;
|
||||
}
|
||||
|
||||
const retryAtMs = Date.parse(trimmedValue);
|
||||
if (Number.isNaN(retryAtMs)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const retryAfterSeconds = Math.ceil((retryAtMs - nowMs) / 1000);
|
||||
return retryAfterSeconds > 0 ? retryAfterSeconds : null;
|
||||
}
|
||||
|
||||
const USAGE_API_HOST = 'api.anthropic.com';
|
||||
const USAGE_API_PATH = '/api/oauth/usage';
|
||||
const USAGE_API_TIMEOUT_MS = 5000;
|
||||
|
||||
async function fetchFromUsageApi(token: string): Promise<string | null> {
|
||||
return new Promise((resolve) => {
|
||||
let settled = false;
|
||||
function getUsageApiProxyUrl(): string | null {
|
||||
const proxyUrl = process.env.HTTPS_PROXY?.trim();
|
||||
if (proxyUrl === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const finish = (value: string | null) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
resolve(value);
|
||||
};
|
||||
return proxyUrl ?? null;
|
||||
}
|
||||
|
||||
const request = https.request({
|
||||
function getUsageApiRequestOptions(token: string): https.RequestOptions | null {
|
||||
const proxyUrl = getUsageApiProxyUrl();
|
||||
|
||||
try {
|
||||
return {
|
||||
hostname: USAGE_API_HOST,
|
||||
path: USAGE_API_PATH,
|
||||
method: 'GET',
|
||||
@@ -195,8 +414,33 @@ async function fetchFromUsageApi(token: string): Promise<string | null> {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'anthropic-beta': 'oauth-2025-04-20'
|
||||
},
|
||||
timeout: 5000
|
||||
}, (response) => {
|
||||
timeout: USAGE_API_TIMEOUT_MS,
|
||||
...(proxyUrl ? { agent: new HttpsProxyAgent(proxyUrl) } : {})
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchFromUsageApi(token: string): Promise<UsageApiFetchResult> {
|
||||
return new Promise((resolve) => {
|
||||
let settled = false;
|
||||
|
||||
const finish = (value: UsageApiFetchResult) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
resolve(value);
|
||||
};
|
||||
|
||||
const requestOptions = getUsageApiRequestOptions(token);
|
||||
if (!requestOptions) {
|
||||
finish({ kind: 'error' });
|
||||
return;
|
||||
}
|
||||
|
||||
const request = https.request(requestOptions, (response) => {
|
||||
let data = '';
|
||||
response.setEncoding('utf8');
|
||||
|
||||
@@ -206,17 +450,26 @@ async function fetchFromUsageApi(token: string): Promise<string | null> {
|
||||
|
||||
response.on('end', () => {
|
||||
if (response.statusCode === 200 && data) {
|
||||
finish(data);
|
||||
finish({ kind: 'success', body: data });
|
||||
return;
|
||||
}
|
||||
finish(null);
|
||||
|
||||
if (response.statusCode === 429) {
|
||||
finish({
|
||||
kind: 'rate-limited',
|
||||
retryAfterSeconds: parseRetryAfterSeconds(response.headers['retry-after']) ?? DEFAULT_RATE_LIMIT_BACKOFF
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
finish({ kind: 'error' });
|
||||
});
|
||||
});
|
||||
|
||||
request.on('error', () => { finish(null); });
|
||||
request.on('error', () => { finish({ kind: 'error' }); });
|
||||
request.on('timeout', () => {
|
||||
request.destroy();
|
||||
finish(null);
|
||||
finish({ kind: 'error' });
|
||||
});
|
||||
request.end();
|
||||
});
|
||||
@@ -231,7 +484,7 @@ export async function fetchUsageData(): Promise<UsageData> {
|
||||
if (!cachedUsageData.error && cacheAge < CACHE_MAX_AGE) {
|
||||
return cachedUsageData;
|
||||
}
|
||||
if (cachedUsageData.error && cacheAge < LOCK_MAX_AGE) {
|
||||
if (cachedUsageData.error && cacheAge < usageErrorCacheMaxAge) {
|
||||
return cachedUsageData;
|
||||
}
|
||||
}
|
||||
@@ -243,9 +496,7 @@ export async function fetchUsageData(): Promise<UsageData> {
|
||||
if (fileAge < CACHE_MAX_AGE) {
|
||||
const fileData = parseCachedUsageData(fs.readFileSync(CACHE_FILE, 'utf8'));
|
||||
if (fileData && !fileData.error) {
|
||||
cachedUsageData = fileData;
|
||||
usageCacheTime = now;
|
||||
return fileData;
|
||||
return cacheUsageData(fileData, now);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
@@ -258,41 +509,31 @@ export async function fetchUsageData(): Promise<UsageData> {
|
||||
return getStaleUsageOrError('no-credentials', now);
|
||||
}
|
||||
|
||||
// Rate limit: only try API once per 30 seconds
|
||||
try {
|
||||
const lockStat = fs.statSync(LOCK_FILE);
|
||||
const lockAge = now - Math.floor(lockStat.mtimeMs / 1000);
|
||||
if (lockAge < LOCK_MAX_AGE) {
|
||||
// Rate limited - return stale cache or timeout error
|
||||
const stale = readStaleUsageCache();
|
||||
if (stale && !stale.error)
|
||||
return stale;
|
||||
return { error: 'timeout' };
|
||||
}
|
||||
} catch {
|
||||
// Lock file doesn't exist - OK to proceed
|
||||
const activeLock = readActiveUsageLock(now);
|
||||
if (activeLock) {
|
||||
return getStaleUsageOrError(
|
||||
activeLock.error,
|
||||
now,
|
||||
Math.max(1, activeLock.blockedUntil - now)
|
||||
);
|
||||
}
|
||||
|
||||
// Touch lock file
|
||||
try {
|
||||
const lockDir = path.dirname(LOCK_FILE);
|
||||
if (!fs.existsSync(lockDir)) {
|
||||
fs.mkdirSync(lockDir, { recursive: true });
|
||||
}
|
||||
fs.writeFileSync(LOCK_FILE, '');
|
||||
} catch {
|
||||
// Ignore lock file errors
|
||||
}
|
||||
writeUsageLock(now + LOCK_MAX_AGE, 'timeout');
|
||||
|
||||
// Fetch from API using Node's https module
|
||||
try {
|
||||
const response = await fetchFromUsageApi(token);
|
||||
|
||||
if (!response) {
|
||||
if (response.kind === 'rate-limited') {
|
||||
writeUsageLock(now + response.retryAfterSeconds, 'rate-limited');
|
||||
return getStaleUsageOrError('rate-limited', now, response.retryAfterSeconds);
|
||||
}
|
||||
|
||||
if (response.kind === 'error') {
|
||||
return getStaleUsageOrError('api-error', now);
|
||||
}
|
||||
|
||||
const usageData = parseUsageApiResponse(response);
|
||||
const usageData = parseUsageApiResponse(response.body);
|
||||
if (!usageData) {
|
||||
return getStaleUsageOrError('parse-error', now);
|
||||
}
|
||||
@@ -304,17 +545,13 @@ export async function fetchUsageData(): Promise<UsageData> {
|
||||
|
||||
// Save to cache
|
||||
try {
|
||||
if (!fs.existsSync(CACHE_DIR)) {
|
||||
fs.mkdirSync(CACHE_DIR, { recursive: true });
|
||||
}
|
||||
ensureCacheDirExists();
|
||||
fs.writeFileSync(CACHE_FILE, JSON.stringify(usageData));
|
||||
} catch {
|
||||
// Ignore cache write errors
|
||||
}
|
||||
|
||||
cachedUsageData = usageData;
|
||||
usageCacheTime = now;
|
||||
return usageData;
|
||||
return cacheUsageData(usageData, now);
|
||||
} catch {
|
||||
return getStaleUsageOrError('parse-error', now);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { z } from 'zod';
|
||||
export const FIVE_HOUR_BLOCK_MS = 5 * 60 * 60 * 1000;
|
||||
export const SEVEN_DAY_WINDOW_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
export const UsageErrorSchema = z.enum(['no-credentials', 'timeout', 'api-error', 'parse-error']);
|
||||
export const UsageErrorSchema = z.enum(['no-credentials', 'timeout', 'rate-limited', 'api-error', 'parse-error']);
|
||||
export type UsageError = z.infer<typeof UsageErrorSchema>;
|
||||
|
||||
export interface UsageData {
|
||||
|
||||
@@ -89,22 +89,24 @@ export function resolveWeeklyUsageWindow(usageData: UsageData, nowMs = Date.now(
|
||||
return getWeeklyUsageWindowFromResetAt(usageData.weeklyResetAt, nowMs);
|
||||
}
|
||||
|
||||
export function formatUsageDuration(durationMs: number): string {
|
||||
export function formatUsageDuration(durationMs: number, compact = false, useDays = true): string {
|
||||
const clampedMs = Math.max(0, durationMs);
|
||||
const elapsedHours = Math.floor(clampedMs / (1000 * 60 * 60));
|
||||
const elapsedMinutes = Math.floor((clampedMs % (1000 * 60 * 60)) / (1000 * 60));
|
||||
const totalHours = Math.floor(clampedMs / (1000 * 60 * 60));
|
||||
const m = Math.floor((clampedMs % (1000 * 60 * 60)) / (1000 * 60));
|
||||
|
||||
if (elapsedMinutes === 0) {
|
||||
return `${elapsedHours}hr`;
|
||||
}
|
||||
|
||||
return `${elapsedHours}hr ${elapsedMinutes}m`;
|
||||
const hLabel = compact ? 'h' : 'hr';
|
||||
const sep = compact ? '' : ' ';
|
||||
const d = useDays ? Math.floor(totalHours / 24) : 0;
|
||||
const h = useDays ? totalHours % 24 : totalHours;
|
||||
const parts = [d > 0 && `${d}d`, h > 0 && `${h}${hLabel}`, m > 0 && `${m}m`].filter(Boolean);
|
||||
return parts.length > 0 ? parts.join(sep) : '0m';
|
||||
}
|
||||
|
||||
export function getUsageErrorMessage(error: UsageError): string {
|
||||
switch (error) {
|
||||
case 'no-credentials': return '[No credentials]';
|
||||
case 'timeout': return '[Timeout]';
|
||||
case 'rate-limited': return '[Rate limited]';
|
||||
case 'api-error': return '[API Error]';
|
||||
case 'parse-error': return '[Parse Error]';
|
||||
}
|
||||
|
||||
@@ -51,7 +51,10 @@ export const WIDGET_MANIFEST: WidgetManifestEntry[] = [
|
||||
{ type: 'weekly-usage', create: () => new widgets.WeeklyUsageWidget() },
|
||||
{ type: 'reset-timer', create: () => new widgets.BlockResetTimerWidget() },
|
||||
{ type: 'weekly-reset-timer', create: () => new widgets.WeeklyResetTimerWidget() },
|
||||
{ type: 'context-bar', create: () => new widgets.ContextBarWidget() }
|
||||
{ type: 'context-bar', create: () => new widgets.ContextBarWidget() },
|
||||
{ type: 'skills', create: () => new widgets.SkillsWidget() },
|
||||
{ type: 'thinking-effort', create: () => new widgets.ThinkingEffortWidget() },
|
||||
{ type: 'vim-mode', create: () => new widgets.VimModeWidget() }
|
||||
];
|
||||
|
||||
export const LAYOUT_WIDGET_MANIFEST: LayoutWidgetManifestEntry[] = [
|
||||
|
||||
@@ -18,8 +18,11 @@ import {
|
||||
getUsageDisplayMode,
|
||||
getUsageDisplayModifierText,
|
||||
getUsageProgressBarWidth,
|
||||
getUsageTimerCustomKeybinds,
|
||||
isUsageCompact,
|
||||
isUsageInverted,
|
||||
isUsageProgressMode,
|
||||
toggleUsageCompact,
|
||||
toggleUsageInverted
|
||||
} from './shared/usage-display';
|
||||
|
||||
@@ -39,25 +42,30 @@ export class BlockResetTimerWidget implements Widget {
|
||||
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
|
||||
return {
|
||||
displayText: this.getDisplayName(),
|
||||
modifierText: getUsageDisplayModifierText(item)
|
||||
modifierText: getUsageDisplayModifierText(item, { includeCompact: true })
|
||||
};
|
||||
}
|
||||
|
||||
handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
|
||||
if (action === 'toggle-progress') {
|
||||
return cycleUsageDisplayMode(item);
|
||||
return cycleUsageDisplayMode(item, ['compact']);
|
||||
}
|
||||
|
||||
if (action === 'toggle-invert') {
|
||||
return toggleUsageInverted(item);
|
||||
}
|
||||
|
||||
if (action === 'toggle-compact') {
|
||||
return toggleUsageCompact(item);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
|
||||
const displayMode = getUsageDisplayMode(item);
|
||||
const inverted = isUsageInverted(item);
|
||||
const compact = isUsageCompact(item);
|
||||
|
||||
if (context.isPreview) {
|
||||
const previewPercent = inverted ? 90.0 : 10.0;
|
||||
@@ -68,7 +76,7 @@ export class BlockResetTimerWidget implements Widget {
|
||||
return formatRawOrLabeledValue(item, 'Reset ', `[${progressBar}] ${previewPercent.toFixed(1)}%`);
|
||||
}
|
||||
|
||||
return formatRawOrLabeledValue(item, 'Reset: ', '4hr 30m');
|
||||
return formatRawOrLabeledValue(item, 'Reset: ', compact ? '4h30m' : '4hr 30m');
|
||||
}
|
||||
|
||||
const usageData = context.usageData ?? {};
|
||||
@@ -90,15 +98,12 @@ export class BlockResetTimerWidget implements Widget {
|
||||
return formatRawOrLabeledValue(item, 'Reset ', `[${progressBar}] ${percentage}%`);
|
||||
}
|
||||
|
||||
const remainingTime = formatUsageDuration(window.remainingMs);
|
||||
const remainingTime = formatUsageDuration(window.remainingMs, compact);
|
||||
return formatRawOrLabeledValue(item, 'Reset: ', remainingTime);
|
||||
}
|
||||
|
||||
getCustomKeybinds(): CustomKeybind[] {
|
||||
return [
|
||||
{ key: 'p', label: '(p)rogress toggle', action: 'toggle-progress' },
|
||||
{ key: 'v', label: 'in(v)ert fill', action: 'toggle-invert' }
|
||||
];
|
||||
getCustomKeybinds(item?: WidgetItem): CustomKeybind[] {
|
||||
return getUsageTimerCustomKeybinds(item);
|
||||
}
|
||||
|
||||
supportsRawValue(): boolean { return true; }
|
||||
|
||||
+15
-10
@@ -17,8 +17,11 @@ import {
|
||||
getUsageDisplayMode,
|
||||
getUsageDisplayModifierText,
|
||||
getUsageProgressBarWidth,
|
||||
getUsageTimerCustomKeybinds,
|
||||
isUsageCompact,
|
||||
isUsageInverted,
|
||||
isUsageProgressMode,
|
||||
toggleUsageCompact,
|
||||
toggleUsageInverted
|
||||
} from './shared/usage-display';
|
||||
|
||||
@@ -38,25 +41,30 @@ export class BlockTimerWidget implements Widget {
|
||||
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
|
||||
return {
|
||||
displayText: this.getDisplayName(),
|
||||
modifierText: getUsageDisplayModifierText(item)
|
||||
modifierText: getUsageDisplayModifierText(item, { includeCompact: true })
|
||||
};
|
||||
}
|
||||
|
||||
handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
|
||||
if (action === 'toggle-progress') {
|
||||
return cycleUsageDisplayMode(item);
|
||||
return cycleUsageDisplayMode(item, ['compact']);
|
||||
}
|
||||
|
||||
if (action === 'toggle-invert') {
|
||||
return toggleUsageInverted(item);
|
||||
}
|
||||
|
||||
if (action === 'toggle-compact') {
|
||||
return toggleUsageCompact(item);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
|
||||
const displayMode = getUsageDisplayMode(item);
|
||||
const inverted = isUsageInverted(item);
|
||||
const compact = isUsageCompact(item);
|
||||
|
||||
if (context.isPreview) {
|
||||
const previewPercent = inverted ? 26.1 : 73.9;
|
||||
@@ -67,7 +75,7 @@ export class BlockTimerWidget implements Widget {
|
||||
return formatRawOrLabeledValue(item, 'Block ', `[${progressBar}] ${previewPercent.toFixed(1)}%`);
|
||||
}
|
||||
|
||||
return formatRawOrLabeledValue(item, 'Block: ', '3hr 45m');
|
||||
return formatRawOrLabeledValue(item, 'Block: ', compact ? '3h45m' : '3hr 45m');
|
||||
}
|
||||
|
||||
const usageData = context.usageData ?? {};
|
||||
@@ -80,7 +88,7 @@ export class BlockTimerWidget implements Widget {
|
||||
return formatRawOrLabeledValue(item, 'Block ', `[${emptyBar}] 0.0%`);
|
||||
}
|
||||
|
||||
return formatRawOrLabeledValue(item, 'Block: ', '0hr 0m');
|
||||
return formatRawOrLabeledValue(item, 'Block: ', compact ? '0h' : '0hr 0m');
|
||||
}
|
||||
|
||||
if (isUsageProgressMode(displayMode)) {
|
||||
@@ -91,15 +99,12 @@ export class BlockTimerWidget implements Widget {
|
||||
return formatRawOrLabeledValue(item, 'Block ', `[${progressBar}] ${percentage}%`);
|
||||
}
|
||||
|
||||
const elapsedTime = formatUsageDuration(window.elapsedMs);
|
||||
const elapsedTime = formatUsageDuration(window.elapsedMs, compact);
|
||||
return formatRawOrLabeledValue(item, 'Block: ', elapsedTime);
|
||||
}
|
||||
|
||||
getCustomKeybinds(): CustomKeybind[] {
|
||||
return [
|
||||
{ key: 'p', label: '(p)rogress toggle', action: 'toggle-progress' },
|
||||
{ key: 'v', label: 'in(v)ert fill', action: 'toggle-invert' }
|
||||
];
|
||||
getCustomKeybinds(item?: WidgetItem): CustomKeybind[] {
|
||||
return getUsageTimerCustomKeybinds(item);
|
||||
}
|
||||
|
||||
supportsRawValue(): boolean { return true; }
|
||||
|
||||
@@ -10,13 +10,26 @@ import {
|
||||
isInsideGitWorkTree,
|
||||
runGit
|
||||
} from '../utils/git';
|
||||
import {
|
||||
encodeGitRefForUrlPath,
|
||||
parseGitHubBaseUrl,
|
||||
renderOsc8Link
|
||||
} from '../utils/hyperlink';
|
||||
|
||||
import { makeModifierText } from './shared/editor-display';
|
||||
import {
|
||||
getHideNoGitKeybinds,
|
||||
getHideNoGitModifierText,
|
||||
handleToggleNoGitAction,
|
||||
isHideNoGitEnabled
|
||||
} from './shared/git-no-git';
|
||||
import {
|
||||
isMetadataFlagEnabled,
|
||||
toggleMetadataFlag
|
||||
} from './shared/metadata';
|
||||
|
||||
const LINK_KEY = 'linkToGitHub';
|
||||
const TOGGLE_LINK_ACTION = 'toggle-link';
|
||||
|
||||
export class GitBranchWidget implements Widget {
|
||||
getDefaultColor(): string { return 'magenta'; }
|
||||
@@ -24,21 +37,34 @@ export class GitBranchWidget implements Widget {
|
||||
getDisplayName(): string { return 'Git Branch'; }
|
||||
getCategory(): string { return 'Git'; }
|
||||
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
|
||||
const isLink = isMetadataFlagEnabled(item, LINK_KEY);
|
||||
const modifiers: string[] = [];
|
||||
const noGitText = getHideNoGitModifierText(item);
|
||||
if (noGitText)
|
||||
modifiers.push('hide \'no git\'');
|
||||
if (isLink)
|
||||
modifiers.push('GitHub link');
|
||||
return {
|
||||
displayText: this.getDisplayName(),
|
||||
modifierText: getHideNoGitModifierText(item)
|
||||
modifierText: makeModifierText(modifiers)
|
||||
};
|
||||
}
|
||||
|
||||
handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
|
||||
if (action === TOGGLE_LINK_ACTION) {
|
||||
return toggleMetadataFlag(item, LINK_KEY);
|
||||
}
|
||||
return handleToggleNoGitAction(action, item);
|
||||
}
|
||||
|
||||
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
|
||||
void settings;
|
||||
const hideNoGit = isHideNoGitEnabled(item);
|
||||
const isLink = isMetadataFlagEnabled(item, LINK_KEY);
|
||||
|
||||
if (context.isPreview) {
|
||||
return item.rawValue ? 'main' : '⎇ main';
|
||||
const text = item.rawValue ? 'main' : '⎇ main';
|
||||
return isLink ? renderOsc8Link('https://github.com/owner/repo/tree/main', text) : text;
|
||||
}
|
||||
|
||||
if (!isInsideGitWorkTree(context)) {
|
||||
@@ -46,10 +72,21 @@ export class GitBranchWidget implements Widget {
|
||||
}
|
||||
|
||||
const branch = this.getGitBranch(context);
|
||||
if (branch)
|
||||
return item.rawValue ? branch : `⎇ ${branch}`;
|
||||
if (!branch) {
|
||||
return hideNoGit ? null : '⎇ no git';
|
||||
}
|
||||
|
||||
return hideNoGit ? null : '⎇ no git';
|
||||
const displayText = item.rawValue ? branch : `⎇ ${branch}`;
|
||||
|
||||
if (isLink) {
|
||||
const remoteUrl = runGit('remote get-url origin', context);
|
||||
const baseUrl = remoteUrl ? parseGitHubBaseUrl(remoteUrl) : null;
|
||||
if (baseUrl) {
|
||||
return renderOsc8Link(`${baseUrl}/tree/${encodeGitRefForUrlPath(branch)}`, displayText);
|
||||
}
|
||||
}
|
||||
|
||||
return displayText;
|
||||
}
|
||||
|
||||
private getGitBranch(context: RenderContext): string | null {
|
||||
@@ -57,7 +94,10 @@ export class GitBranchWidget implements Widget {
|
||||
}
|
||||
|
||||
getCustomKeybinds(): CustomKeybind[] {
|
||||
return getHideNoGitKeybinds();
|
||||
return [
|
||||
...getHideNoGitKeybinds(),
|
||||
{ key: 'l', label: '(l)ink to GitHub', action: TOGGLE_LINK_ACTION }
|
||||
];
|
||||
}
|
||||
|
||||
supportsRawValue(): boolean { return true; }
|
||||
|
||||
@@ -10,13 +10,29 @@ import {
|
||||
isInsideGitWorkTree,
|
||||
runGit
|
||||
} from '../utils/git';
|
||||
import type { IdeLinkMode } from '../utils/hyperlink';
|
||||
import {
|
||||
IDE_LINK_MODES,
|
||||
buildIdeFileUrl,
|
||||
renderOsc8Link
|
||||
} from '../utils/hyperlink';
|
||||
|
||||
import { makeModifierText } from './shared/editor-display';
|
||||
import {
|
||||
getHideNoGitKeybinds,
|
||||
getHideNoGitModifierText,
|
||||
handleToggleNoGitAction,
|
||||
isHideNoGitEnabled
|
||||
} from './shared/git-no-git';
|
||||
import { isMetadataFlagEnabled } from './shared/metadata';
|
||||
|
||||
const IDE_LINK_KEY = 'linkToIDE';
|
||||
const LEGACY_CURSOR_LINK_KEY = 'linkToCursor';
|
||||
const TOGGLE_LINK_ACTION = 'toggle-link';
|
||||
const IDE_LINK_LABELS: Record<IdeLinkMode, string> = {
|
||||
vscode: 'link-vscode',
|
||||
cursor: 'link-cursor'
|
||||
};
|
||||
|
||||
export class GitRootDirWidget implements Widget {
|
||||
getDefaultColor(): string { return 'cyan'; }
|
||||
@@ -24,21 +40,33 @@ export class GitRootDirWidget implements Widget {
|
||||
getDisplayName(): string { return 'Git Root Dir'; }
|
||||
getCategory(): string { return 'Git'; }
|
||||
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
|
||||
const ideLinkMode = this.getIdeLinkMode(item);
|
||||
const modifiers: string[] = [];
|
||||
const noGitText = getHideNoGitModifierText(item);
|
||||
if (noGitText)
|
||||
modifiers.push('hide \'no git\'');
|
||||
if (ideLinkMode)
|
||||
modifiers.push(IDE_LINK_LABELS[ideLinkMode]);
|
||||
return {
|
||||
displayText: this.getDisplayName(),
|
||||
modifierText: getHideNoGitModifierText(item)
|
||||
modifierText: makeModifierText(modifiers)
|
||||
};
|
||||
}
|
||||
|
||||
handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
|
||||
if (action === TOGGLE_LINK_ACTION) {
|
||||
return this.cycleIdeLinkMode(item);
|
||||
}
|
||||
return handleToggleNoGitAction(action, item);
|
||||
}
|
||||
|
||||
render(item: WidgetItem, context: RenderContext, _settings: Settings): string | null {
|
||||
const hideNoGit = isHideNoGitEnabled(item);
|
||||
const ideLinkMode = this.getIdeLinkMode(item);
|
||||
|
||||
if (context.isPreview) {
|
||||
return 'my-repo';
|
||||
const name = 'my-repo';
|
||||
return ideLinkMode ? renderOsc8Link(buildIdeFileUrl('/Users/example/my-repo', ideLinkMode), name) : name;
|
||||
}
|
||||
|
||||
if (!isInsideGitWorkTree(context)) {
|
||||
@@ -46,11 +74,17 @@ export class GitRootDirWidget implements Widget {
|
||||
}
|
||||
|
||||
const rootDir = this.getGitRootDir(context);
|
||||
if (rootDir) {
|
||||
return this.getRootDirName(rootDir);
|
||||
if (!rootDir) {
|
||||
return hideNoGit ? null : 'no git';
|
||||
}
|
||||
|
||||
return hideNoGit ? null : 'no git';
|
||||
const name = this.getRootDirName(rootDir);
|
||||
|
||||
if (ideLinkMode) {
|
||||
return renderOsc8Link(buildIdeFileUrl(rootDir, ideLinkMode), name);
|
||||
}
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
private getGitRootDir(context: RenderContext): string | null {
|
||||
@@ -66,9 +100,47 @@ export class GitRootDirWidget implements Widget {
|
||||
}
|
||||
|
||||
getCustomKeybinds(): CustomKeybind[] {
|
||||
return getHideNoGitKeybinds();
|
||||
return [
|
||||
...getHideNoGitKeybinds(),
|
||||
{ key: 'l', label: '(l)ink to IDE', action: TOGGLE_LINK_ACTION }
|
||||
];
|
||||
}
|
||||
|
||||
supportsRawValue(): boolean { return false; }
|
||||
supportsColors(item: WidgetItem): boolean { return true; }
|
||||
|
||||
private getIdeLinkMode(item: WidgetItem): IdeLinkMode | null {
|
||||
const configuredMode = item.metadata?.[IDE_LINK_KEY];
|
||||
if (configuredMode && IDE_LINK_MODES.includes(configuredMode as IdeLinkMode)) {
|
||||
return configuredMode as IdeLinkMode;
|
||||
}
|
||||
|
||||
if (isMetadataFlagEnabled(item, LEGACY_CURSOR_LINK_KEY)) {
|
||||
return 'cursor';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private cycleIdeLinkMode(item: WidgetItem): WidgetItem {
|
||||
const currentMode = this.getIdeLinkMode(item);
|
||||
const currentIndex = currentMode ? IDE_LINK_MODES.indexOf(currentMode) : -1;
|
||||
const nextMode = currentIndex === IDE_LINK_MODES.length - 1 ? null : (IDE_LINK_MODES[currentIndex + 1] ?? null);
|
||||
const {
|
||||
[IDE_LINK_KEY]: removedIdeLink,
|
||||
[LEGACY_CURSOR_LINK_KEY]: removedLegacyLink,
|
||||
...restMetadata
|
||||
} = item.metadata ?? {};
|
||||
|
||||
void removedIdeLink;
|
||||
void removedLegacyLink;
|
||||
|
||||
return {
|
||||
...item,
|
||||
metadata: nextMode ? {
|
||||
...restMetadata,
|
||||
[IDE_LINK_KEY]: nextMode
|
||||
} : (Object.keys(restMetadata).length > 0 ? restMetadata : undefined)
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
WidgetEditorProps,
|
||||
WidgetItem
|
||||
} from '../types/Widget';
|
||||
import { renderOsc8Link } from '../utils/hyperlink';
|
||||
import { shouldInsertInput } from '../utils/input-guards';
|
||||
|
||||
function isValidHttpUrl(url: string): boolean {
|
||||
@@ -60,10 +61,6 @@ function buildMetadata(widget: WidgetItem, urlValue: string, textValue: string):
|
||||
};
|
||||
}
|
||||
|
||||
function renderOsc8Link(url: string, text: string): string {
|
||||
return `\x1b]8;;${url}\x1b\\${text}\x1b]8;;\x1b\\`;
|
||||
}
|
||||
|
||||
function getLinkLabel(item: WidgetItem): { url: string; label: string } {
|
||||
const url = item.metadata?.url?.trim() ?? '';
|
||||
const metadataText = item.metadata?.text?.trim();
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
cycleUsageDisplayMode,
|
||||
getUsageDisplayMode,
|
||||
getUsageDisplayModifierText,
|
||||
getUsagePercentCustomKeybinds,
|
||||
getUsageProgressBarWidth,
|
||||
isUsageInverted,
|
||||
isUsageProgressMode,
|
||||
@@ -81,11 +82,8 @@ export class SessionUsageWidget implements Widget {
|
||||
return formatRawOrLabeledValue(item, 'Session: ', `${percent.toFixed(1)}%`);
|
||||
}
|
||||
|
||||
getCustomKeybinds(): CustomKeybind[] {
|
||||
return [
|
||||
{ key: 'p', label: '(p)rogress toggle', action: 'toggle-progress' },
|
||||
{ key: 'v', label: 'in(v)ert fill', action: 'toggle-invert' }
|
||||
];
|
||||
getCustomKeybinds(item?: WidgetItem): CustomKeybind[] {
|
||||
return getUsagePercentCustomKeybinds(item);
|
||||
}
|
||||
|
||||
supportsRawValue(): boolean { return true; }
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
import {
|
||||
Box,
|
||||
Text,
|
||||
useInput
|
||||
} from 'ink';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import type { RenderContext } from '../types/RenderContext';
|
||||
import type { Settings } from '../types/Settings';
|
||||
import type {
|
||||
CustomKeybind,
|
||||
Widget,
|
||||
WidgetEditorDisplay,
|
||||
WidgetEditorProps,
|
||||
WidgetItem
|
||||
} from '../types/Widget';
|
||||
import type { WidgetHookDef } from '../utils/hooks';
|
||||
import { shouldInsertInput } from '../utils/input-guards';
|
||||
|
||||
import { makeModifierText } from './shared/editor-display';
|
||||
import {
|
||||
isMetadataFlagEnabled,
|
||||
removeMetadataKeys,
|
||||
toggleMetadataFlag
|
||||
} from './shared/metadata';
|
||||
|
||||
type Mode = 'current' | 'count' | 'list';
|
||||
const MODES: Mode[] = ['current', 'count', 'list'];
|
||||
const MODE_LABELS: Record<Mode, string> = { current: 'last used', count: 'total count', list: 'unique list' };
|
||||
const HIDE_WHEN_EMPTY_KEY = 'hideWhenEmpty';
|
||||
const LIST_LIMIT_KEY = 'listLimit';
|
||||
const TOGGLE_HIDE_EMPTY_ACTION = 'toggle-hide-empty';
|
||||
const EDIT_LIST_LIMIT_ACTION = 'edit-list-limit';
|
||||
|
||||
function parseListLimit(item: WidgetItem): number {
|
||||
const parsed = parseInt(item.metadata?.[LIST_LIMIT_KEY] ?? '0', 10);
|
||||
if (Number.isNaN(parsed) || parsed < 0) {
|
||||
return 0;
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function setListLimit(item: WidgetItem, limit: number): WidgetItem {
|
||||
if (limit <= 0) {
|
||||
const { [LIST_LIMIT_KEY]: removedLimit, ...restMetadata } = item.metadata ?? {};
|
||||
void removedLimit;
|
||||
return {
|
||||
...item,
|
||||
metadata: Object.keys(restMetadata).length > 0 ? restMetadata : undefined
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
metadata: {
|
||||
...item.metadata,
|
||||
[LIST_LIMIT_KEY]: limit.toString()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export class SkillsWidget implements Widget {
|
||||
getDefaultColor(): string { return 'magenta'; }
|
||||
getDescription(): string { return 'Shows Claude Code skill invocations from hook data'; }
|
||||
getDisplayName(): string { return 'Skills'; }
|
||||
getCategory(): string { return 'Session'; }
|
||||
supportsRawValue(): boolean { return true; }
|
||||
supportsColors(): boolean { return true; }
|
||||
|
||||
getHooks(): WidgetHookDef[] {
|
||||
return [
|
||||
{ event: 'PreToolUse', matcher: 'Skill' },
|
||||
{ event: 'UserPromptSubmit' }
|
||||
];
|
||||
}
|
||||
|
||||
getCustomKeybinds(item?: WidgetItem): CustomKeybind[] {
|
||||
const keybinds: CustomKeybind[] = [
|
||||
{ key: 'v', label: '(v)iew: last/count/list', action: 'cycle-mode' },
|
||||
{ key: 'h', label: '(h)ide when empty', action: TOGGLE_HIDE_EMPTY_ACTION }
|
||||
];
|
||||
|
||||
if (item && this.getMode(item) === 'list') {
|
||||
keybinds.push({ key: 'l', label: '(l)imit', action: EDIT_LIST_LIMIT_ACTION });
|
||||
}
|
||||
|
||||
return keybinds;
|
||||
}
|
||||
|
||||
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
|
||||
const modifiers = [MODE_LABELS[this.getMode(item)]];
|
||||
if (this.getMode(item) === 'list') {
|
||||
const limit = parseListLimit(item);
|
||||
if (limit > 0) {
|
||||
modifiers.push(`limit: ${limit}`);
|
||||
}
|
||||
}
|
||||
if (this.isHideWhenEmptyEnabled(item)) {
|
||||
modifiers.push('hide when empty');
|
||||
}
|
||||
return { displayText: 'Skills', modifierText: makeModifierText(modifiers) };
|
||||
}
|
||||
|
||||
handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
|
||||
if (action === 'cycle-mode') {
|
||||
const next = MODES[(MODES.indexOf(this.getMode(item)) + 1) % MODES.length] ?? 'current';
|
||||
const nextItem = next === 'list' ? item : removeMetadataKeys(item, [LIST_LIMIT_KEY]);
|
||||
return { ...nextItem, metadata: { ...nextItem.metadata, mode: next } };
|
||||
}
|
||||
if (action === TOGGLE_HIDE_EMPTY_ACTION) {
|
||||
return toggleMetadataFlag(item, HIDE_WHEN_EMPTY_KEY);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
renderEditor(props: WidgetEditorProps): React.ReactElement {
|
||||
return <SkillsEditor {...props} />;
|
||||
}
|
||||
|
||||
render(item: WidgetItem, context: RenderContext, _settings: Settings): string | null {
|
||||
const mode = this.getMode(item);
|
||||
const raw = item.rawValue;
|
||||
const hideWhenEmpty = this.isHideWhenEmptyEnabled(item);
|
||||
|
||||
if (context.isPreview) {
|
||||
if (mode === 'current') {
|
||||
return raw ? 'commit' : 'Skill: commit';
|
||||
}
|
||||
if (mode === 'count') {
|
||||
return raw ? '5' : 'Skills: 5';
|
||||
}
|
||||
return raw ? 'commit, review-pr' : 'Skills: commit, review-pr';
|
||||
}
|
||||
|
||||
if (mode === 'current') {
|
||||
const currentSkill = context.skillsMetrics?.lastSkill;
|
||||
if (!currentSkill) {
|
||||
if (hideWhenEmpty) {
|
||||
return null;
|
||||
}
|
||||
return raw ? 'none' : 'Skill: none';
|
||||
}
|
||||
return raw ? currentSkill : `Skill: ${currentSkill}`;
|
||||
}
|
||||
if (mode === 'count') {
|
||||
const total = context.skillsMetrics?.totalInvocations ?? 0;
|
||||
if (hideWhenEmpty && total === 0) {
|
||||
return null;
|
||||
}
|
||||
return raw ? String(total) : `Skills: ${total}`;
|
||||
}
|
||||
|
||||
const uniqueSkills = context.skillsMetrics?.uniqueSkills ?? [];
|
||||
if (uniqueSkills.length === 0) {
|
||||
if (hideWhenEmpty) {
|
||||
return null;
|
||||
}
|
||||
return raw ? 'none' : 'Skills: none';
|
||||
}
|
||||
|
||||
const limit = parseListLimit(item);
|
||||
const visibleSkills = limit > 0 ? uniqueSkills.slice(0, limit) : uniqueSkills;
|
||||
const list = visibleSkills.join(', ');
|
||||
return raw ? list : `Skills: ${list}`;
|
||||
}
|
||||
|
||||
private getMode(item: WidgetItem): Mode {
|
||||
const mode = item.metadata?.mode;
|
||||
return mode && MODES.includes(mode as Mode) ? mode as Mode : 'current';
|
||||
}
|
||||
|
||||
private isHideWhenEmptyEnabled(item: WidgetItem): boolean {
|
||||
return isMetadataFlagEnabled(item, HIDE_WHEN_EMPTY_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
const SkillsEditor: React.FC<WidgetEditorProps> = ({ widget, onComplete, onCancel, action }) => {
|
||||
const [limitInput, setLimitInput] = useState(() => parseListLimit(widget).toString());
|
||||
|
||||
useInput((input, key) => {
|
||||
if (action !== EDIT_LIST_LIMIT_ACTION) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (key.return) {
|
||||
const parsed = parseInt(limitInput, 10);
|
||||
const limit = Number.isNaN(parsed) || parsed < 0 ? 0 : parsed;
|
||||
onComplete(setListLimit(widget, limit));
|
||||
} else if (key.escape) {
|
||||
onCancel();
|
||||
} else if (key.backspace) {
|
||||
setLimitInput(limitInput.slice(0, -1));
|
||||
} else if (shouldInsertInput(input, key) && /\d/.test(input)) {
|
||||
setLimitInput(limitInput + input);
|
||||
}
|
||||
});
|
||||
|
||||
if (action === EDIT_LIST_LIMIT_ACTION) {
|
||||
return (
|
||||
<Box flexDirection='column'>
|
||||
<Box>
|
||||
<Text>Enter max skills to show (0 for unlimited): </Text>
|
||||
<Text>{limitInput}</Text>
|
||||
<Text backgroundColor='gray' color='black'>{' '}</Text>
|
||||
</Box>
|
||||
<Text dimColor>Press Enter to save, ESC to cancel</Text>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return <Text>Unknown editor mode</Text>;
|
||||
};
|
||||
@@ -0,0 +1,66 @@
|
||||
import type { RenderContext } from '../types/RenderContext';
|
||||
import type { Settings } from '../types/Settings';
|
||||
import type {
|
||||
Widget,
|
||||
WidgetEditorDisplay,
|
||||
WidgetItem
|
||||
} from '../types/Widget';
|
||||
import { loadClaudeSettingsSync } from '../utils/claude-settings';
|
||||
import { getTranscriptThinkingEffort } from '../utils/jsonl';
|
||||
|
||||
export type ThinkingEffortLevel = 'low' | 'medium' | 'high' | 'max';
|
||||
|
||||
/**
|
||||
* Resolve thinking effort from transcript and settings.
|
||||
*/
|
||||
function normalizeThinkingEffort(value: string | undefined): ThinkingEffortLevel | undefined {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const normalized = value.toLowerCase();
|
||||
if (normalized === 'low' || normalized === 'medium' || normalized === 'high' || normalized === 'max') {
|
||||
return normalized;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveThinkingEffortFromSettings(): ThinkingEffortLevel | undefined {
|
||||
try {
|
||||
const settings = loadClaudeSettingsSync({ logErrors: false });
|
||||
return normalizeThinkingEffort(settings.effortLevel);
|
||||
} catch {
|
||||
// Settings unavailable, return undefined
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveThinkingEffort(context: RenderContext): ThinkingEffortLevel {
|
||||
return getTranscriptThinkingEffort(context.data?.transcript_path)
|
||||
?? resolveThinkingEffortFromSettings()
|
||||
?? 'medium';
|
||||
}
|
||||
|
||||
export class ThinkingEffortWidget implements Widget {
|
||||
getDefaultColor(): string { return 'magenta'; }
|
||||
getDescription(): string { return 'Displays the current thinking effort level (low, medium, high, max).\nMay be incorrect when multiple Claude Code sessions are running due to current Claude Code limitations.'; }
|
||||
getDisplayName(): string { return 'Thinking Effort'; }
|
||||
getCategory(): string { return 'Core'; }
|
||||
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
|
||||
return { displayText: this.getDisplayName() };
|
||||
}
|
||||
|
||||
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
|
||||
if (context.isPreview) {
|
||||
return item.rawValue ? 'high' : 'Thinking: high';
|
||||
}
|
||||
|
||||
const effort = resolveThinkingEffort(context);
|
||||
return item.rawValue ? effort : `Thinking: ${effort}`;
|
||||
}
|
||||
|
||||
supportsRawValue(): boolean { return true; }
|
||||
supportsColors(item: WidgetItem): boolean { return true; }
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import type { RenderContext } from '../types/RenderContext';
|
||||
import type { Settings } from '../types/Settings';
|
||||
import type {
|
||||
CustomKeybind,
|
||||
Widget,
|
||||
WidgetEditorDisplay,
|
||||
WidgetItem
|
||||
} from '../types/Widget';
|
||||
|
||||
const VIM_ICON = 'v';
|
||||
const VIM_NERD_FONT_ICON = '\uE62B';
|
||||
|
||||
const FORMATS = ['icon-dash-letter', 'icon-letter', 'icon', 'letter', 'word'] as const;
|
||||
type VimFormat = typeof FORMATS[number];
|
||||
|
||||
const DEFAULT_FORMAT: VimFormat = 'icon-dash-letter';
|
||||
const CYCLE_FORMAT_ACTION = 'cycle-format';
|
||||
const TOGGLE_NERD_FONT_ACTION = 'toggle-nerd-font';
|
||||
const NERD_FONT_METADATA_KEY = 'nerdFont';
|
||||
|
||||
function getFormat(item: WidgetItem): VimFormat {
|
||||
const f = item.metadata?.format;
|
||||
return (FORMATS as readonly string[]).includes(f ?? '') ? (f as VimFormat) : DEFAULT_FORMAT;
|
||||
}
|
||||
|
||||
function setFormat(item: WidgetItem, format: VimFormat): WidgetItem {
|
||||
if (format === DEFAULT_FORMAT) {
|
||||
const { format: removedFormat, ...restMetadata } = item.metadata ?? {};
|
||||
void removedFormat;
|
||||
|
||||
return {
|
||||
...item,
|
||||
metadata: Object.keys(restMetadata).length > 0 ? restMetadata : undefined
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
metadata: {
|
||||
...(item.metadata ?? {}),
|
||||
format
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function isNerdFontEnabled(item: WidgetItem): boolean {
|
||||
return item.metadata?.[NERD_FONT_METADATA_KEY] === 'true';
|
||||
}
|
||||
|
||||
function toggleNerdFont(item: WidgetItem): WidgetItem {
|
||||
if (!isNerdFontEnabled(item)) {
|
||||
return {
|
||||
...item,
|
||||
metadata: {
|
||||
...(item.metadata ?? {}),
|
||||
[NERD_FONT_METADATA_KEY]: 'true'
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const { [NERD_FONT_METADATA_KEY]: removedNerdFont, ...restMetadata } = item.metadata ?? {};
|
||||
void removedNerdFont;
|
||||
|
||||
return {
|
||||
...item,
|
||||
metadata: Object.keys(restMetadata).length > 0 ? restMetadata : undefined
|
||||
};
|
||||
}
|
||||
|
||||
function formatMode(mode: string, format: VimFormat, icon: string): string {
|
||||
const letter = mode === 'NORMAL' ? 'N' : mode === 'INSERT' ? 'I' : (mode[0] ?? mode);
|
||||
switch (format) {
|
||||
case 'icon-dash-letter': return `${icon}-${letter}`;
|
||||
case 'icon-letter': return `${icon} ${letter}`;
|
||||
case 'icon': return icon;
|
||||
case 'letter': return letter;
|
||||
case 'word': return mode;
|
||||
}
|
||||
}
|
||||
|
||||
export class VimModeWidget implements Widget {
|
||||
getDefaultColor(): string { return 'green'; }
|
||||
getDescription(): string { return 'Displays current vim editor mode'; }
|
||||
getDisplayName(): string { return 'Vim Mode'; }
|
||||
getCategory(): string { return 'Core'; }
|
||||
|
||||
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
|
||||
const modifiers: string[] = [getFormat(item)];
|
||||
if (isNerdFontEnabled(item)) {
|
||||
modifiers.push('nerd font');
|
||||
}
|
||||
|
||||
return {
|
||||
displayText: this.getDisplayName(),
|
||||
modifierText: `(${modifiers.join(', ')})`
|
||||
};
|
||||
}
|
||||
|
||||
handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
|
||||
if (action === CYCLE_FORMAT_ACTION) {
|
||||
const currentFormat = getFormat(item);
|
||||
const nextFormat = FORMATS[(FORMATS.indexOf(currentFormat) + 1) % FORMATS.length] ?? DEFAULT_FORMAT;
|
||||
|
||||
return setFormat(item, nextFormat);
|
||||
}
|
||||
|
||||
if (action === TOGGLE_NERD_FONT_ACTION) {
|
||||
return toggleNerdFont(item);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
render(item: WidgetItem, context: RenderContext, _settings: Settings): string | null {
|
||||
const format = getFormat(item);
|
||||
const icon = isNerdFontEnabled(item) ? VIM_NERD_FONT_ICON : VIM_ICON;
|
||||
|
||||
if (context.isPreview)
|
||||
return formatMode('NORMAL', format, icon);
|
||||
|
||||
const mode = context.data?.vim?.mode;
|
||||
if (mode === undefined)
|
||||
return null;
|
||||
|
||||
return formatMode(mode, format, icon);
|
||||
}
|
||||
|
||||
getCustomKeybinds(): CustomKeybind[] {
|
||||
return [
|
||||
{ key: 'f', label: '(f)ormat', action: CYCLE_FORMAT_ACTION },
|
||||
{ key: 'n', label: '(n)erd font', action: TOGGLE_NERD_FONT_ACTION }
|
||||
];
|
||||
}
|
||||
|
||||
supportsRawValue(): boolean { return false; }
|
||||
supportsColors(_item: WidgetItem): boolean { return true; }
|
||||
}
|
||||
@@ -12,14 +12,21 @@ import {
|
||||
resolveWeeklyUsageWindow
|
||||
} from '../utils/usage';
|
||||
|
||||
import { makeModifierText } from './shared/editor-display';
|
||||
import {
|
||||
isMetadataFlagEnabled,
|
||||
toggleMetadataFlag
|
||||
} from './shared/metadata';
|
||||
import { formatRawOrLabeledValue } from './shared/raw-or-labeled';
|
||||
import {
|
||||
cycleUsageDisplayMode,
|
||||
getUsageDisplayMode,
|
||||
getUsageDisplayModifierText,
|
||||
getUsageProgressBarWidth,
|
||||
getUsageTimerCustomKeybinds,
|
||||
isUsageCompact,
|
||||
isUsageInverted,
|
||||
isUsageProgressMode,
|
||||
toggleUsageCompact,
|
||||
toggleUsageInverted
|
||||
} from './shared/usage-display';
|
||||
|
||||
@@ -30,6 +37,43 @@ function makeTimerProgressBar(percent: number, width: number): string {
|
||||
return '█'.repeat(filledWidth) + '░'.repeat(emptyWidth);
|
||||
}
|
||||
|
||||
const WEEKLY_PREVIEW_DURATION_MS = 36.5 * 60 * 60 * 1000;
|
||||
|
||||
function isWeeklyResetHoursOnly(item: WidgetItem): boolean {
|
||||
return isMetadataFlagEnabled(item, 'hours');
|
||||
}
|
||||
|
||||
function toggleWeeklyResetHoursOnly(item: WidgetItem): WidgetItem {
|
||||
return toggleMetadataFlag(item, 'hours');
|
||||
}
|
||||
|
||||
function getWeeklyResetModifierText(item: WidgetItem): string | undefined {
|
||||
const displayMode = getUsageDisplayMode(item);
|
||||
const modifiers: string[] = [];
|
||||
|
||||
if (displayMode === 'progress') {
|
||||
modifiers.push('progress bar');
|
||||
} else if (displayMode === 'progress-short') {
|
||||
modifiers.push('short bar');
|
||||
}
|
||||
|
||||
if (isUsageInverted(item)) {
|
||||
modifiers.push('inverted');
|
||||
}
|
||||
|
||||
if (!isUsageProgressMode(displayMode)) {
|
||||
if (isUsageCompact(item)) {
|
||||
modifiers.push('compact');
|
||||
}
|
||||
|
||||
if (isWeeklyResetHoursOnly(item)) {
|
||||
modifiers.push('hours only');
|
||||
}
|
||||
}
|
||||
|
||||
return makeModifierText(modifiers);
|
||||
}
|
||||
|
||||
export class WeeklyResetTimerWidget implements Widget {
|
||||
getDefaultColor(): string { return 'brightBlue'; }
|
||||
getDescription(): string { return 'Shows time remaining until weekly usage reset'; }
|
||||
@@ -39,25 +83,35 @@ export class WeeklyResetTimerWidget implements Widget {
|
||||
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay {
|
||||
return {
|
||||
displayText: this.getDisplayName(),
|
||||
modifierText: getUsageDisplayModifierText(item)
|
||||
modifierText: getWeeklyResetModifierText(item)
|
||||
};
|
||||
}
|
||||
|
||||
handleEditorAction(action: string, item: WidgetItem): WidgetItem | null {
|
||||
if (action === 'toggle-progress') {
|
||||
return cycleUsageDisplayMode(item);
|
||||
return cycleUsageDisplayMode(item, ['compact', 'hours']);
|
||||
}
|
||||
|
||||
if (action === 'toggle-invert') {
|
||||
return toggleUsageInverted(item);
|
||||
}
|
||||
|
||||
if (action === 'toggle-compact') {
|
||||
return toggleUsageCompact(item);
|
||||
}
|
||||
|
||||
if (action === 'toggle-hours') {
|
||||
return toggleWeeklyResetHoursOnly(item);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
render(item: WidgetItem, context: RenderContext, settings: Settings): string | null {
|
||||
const displayMode = getUsageDisplayMode(item);
|
||||
const inverted = isUsageInverted(item);
|
||||
const compact = isUsageCompact(item);
|
||||
const useDays = !isWeeklyResetHoursOnly(item);
|
||||
|
||||
if (context.isPreview) {
|
||||
const previewPercent = inverted ? 90.0 : 10.0;
|
||||
@@ -68,7 +122,7 @@ export class WeeklyResetTimerWidget implements Widget {
|
||||
return formatRawOrLabeledValue(item, 'Weekly Reset ', `[${progressBar}] ${previewPercent.toFixed(1)}%`);
|
||||
}
|
||||
|
||||
return formatRawOrLabeledValue(item, 'Weekly Reset: ', '36hr 30m');
|
||||
return formatRawOrLabeledValue(item, 'Weekly Reset: ', formatUsageDuration(WEEKLY_PREVIEW_DURATION_MS, compact, useDays));
|
||||
}
|
||||
|
||||
const usageData = context.usageData ?? {};
|
||||
@@ -90,15 +144,18 @@ export class WeeklyResetTimerWidget implements Widget {
|
||||
return formatRawOrLabeledValue(item, 'Weekly Reset ', `[${progressBar}] ${percentage}%`);
|
||||
}
|
||||
|
||||
const remainingTime = formatUsageDuration(window.remainingMs);
|
||||
const remainingTime = formatUsageDuration(window.remainingMs, compact, useDays);
|
||||
return formatRawOrLabeledValue(item, 'Weekly Reset: ', remainingTime);
|
||||
}
|
||||
|
||||
getCustomKeybinds(): CustomKeybind[] {
|
||||
return [
|
||||
{ key: 'p', label: '(p)rogress toggle', action: 'toggle-progress' },
|
||||
{ key: 'v', label: 'in(v)ert fill', action: 'toggle-invert' }
|
||||
];
|
||||
getCustomKeybinds(item?: WidgetItem): CustomKeybind[] {
|
||||
const keybinds = getUsageTimerCustomKeybinds(item);
|
||||
|
||||
if (!item || !isUsageProgressMode(getUsageDisplayMode(item))) {
|
||||
keybinds.push({ key: 'h', label: '(h)ours only', action: 'toggle-hours' });
|
||||
}
|
||||
|
||||
return keybinds;
|
||||
}
|
||||
|
||||
supportsRawValue(): boolean { return true; }
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
cycleUsageDisplayMode,
|
||||
getUsageDisplayMode,
|
||||
getUsageDisplayModifierText,
|
||||
getUsagePercentCustomKeybinds,
|
||||
getUsageProgressBarWidth,
|
||||
isUsageInverted,
|
||||
isUsageProgressMode,
|
||||
@@ -81,11 +82,8 @@ export class WeeklyUsageWidget implements Widget {
|
||||
return formatRawOrLabeledValue(item, 'Weekly: ', `${percent.toFixed(1)}%`);
|
||||
}
|
||||
|
||||
getCustomKeybinds(): CustomKeybind[] {
|
||||
return [
|
||||
{ key: 'p', label: '(p)rogress toggle', action: 'toggle-progress' },
|
||||
{ key: 'v', label: 'in(v)ert fill', action: 'toggle-invert' }
|
||||
];
|
||||
getCustomKeybinds(item?: WidgetItem): CustomKeybind[] {
|
||||
return getUsagePercentCustomKeybinds(item);
|
||||
}
|
||||
|
||||
supportsRawValue(): boolean { return true; }
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
import type { RenderContext } from '../../types/RenderContext';
|
||||
import { DEFAULT_SETTINGS } from '../../types/Settings';
|
||||
import type { WidgetItem } from '../../types/Widget';
|
||||
import { renderOsc8Link } from '../../utils/hyperlink';
|
||||
import { GitBranchWidget } from '../GitBranch';
|
||||
|
||||
vi.mock('child_process', () => ({ execSync: vi.fn() }));
|
||||
@@ -25,6 +26,8 @@ function render(options: {
|
||||
cwd?: string;
|
||||
hideNoGit?: boolean;
|
||||
isPreview?: boolean;
|
||||
linkToGitHub?: boolean;
|
||||
metadata?: Record<string, string>;
|
||||
rawValue?: boolean;
|
||||
} = {}) {
|
||||
const widget = new GitBranchWidget();
|
||||
@@ -32,11 +35,16 @@ function render(options: {
|
||||
isPreview: options.isPreview,
|
||||
data: options.cwd ? { cwd: options.cwd } : undefined
|
||||
};
|
||||
const metadata = {
|
||||
...options.metadata,
|
||||
...(options.hideNoGit ? { hideNoGit: 'true' } : {}),
|
||||
...(options.linkToGitHub ? { linkToGitHub: 'true' } : {})
|
||||
};
|
||||
const item: WidgetItem = {
|
||||
id: 'git-branch',
|
||||
type: 'git-branch',
|
||||
rawValue: options.rawValue,
|
||||
metadata: options.hideNoGit ? { hideNoGit: 'true' } : undefined
|
||||
metadata: Object.keys(metadata).length > 0 ? metadata : undefined
|
||||
};
|
||||
|
||||
return widget.render(item, context, DEFAULT_SETTINGS);
|
||||
@@ -79,6 +87,17 @@ describe('GitBranchWidget', () => {
|
||||
expect(render({ rawValue: true })).toBe('feature/worktree');
|
||||
});
|
||||
|
||||
it('should render encoded GitHub branch links', () => {
|
||||
mockExecSync.mockReturnValueOnce('true\n');
|
||||
mockExecSync.mockReturnValueOnce('feature/issue#1');
|
||||
mockExecSync.mockReturnValueOnce('ssh://git@github.com/owner/repo.git');
|
||||
|
||||
expect(render({ linkToGitHub: true })).toBe(renderOsc8Link(
|
||||
'https://github.com/owner/repo/tree/feature/issue%231',
|
||||
'⎇ feature/issue#1'
|
||||
));
|
||||
});
|
||||
|
||||
it('should render no git when probe returns false', () => {
|
||||
mockExecSync.mockReturnValue('false\n');
|
||||
|
||||
@@ -103,4 +122,12 @@ describe('GitBranchWidget', () => {
|
||||
|
||||
expect(render()).toBe('⎇ no git');
|
||||
});
|
||||
|
||||
it('should keep plain text when GitHub remote cannot be parsed', () => {
|
||||
mockExecSync.mockReturnValueOnce('true\n');
|
||||
mockExecSync.mockReturnValueOnce('feature/worktree');
|
||||
mockExecSync.mockReturnValueOnce('https://gitlab.com/owner/repo.git');
|
||||
|
||||
expect(render({ linkToGitHub: true })).toBe('⎇ feature/worktree');
|
||||
});
|
||||
});
|
||||
@@ -10,6 +10,10 @@ import {
|
||||
import type { RenderContext } from '../../types/RenderContext';
|
||||
import { DEFAULT_SETTINGS } from '../../types/Settings';
|
||||
import type { WidgetItem } from '../../types/Widget';
|
||||
import {
|
||||
buildIdeFileUrl,
|
||||
renderOsc8Link
|
||||
} from '../../utils/hyperlink';
|
||||
import { GitRootDirWidget } from '../GitRootDir';
|
||||
|
||||
vi.mock('child_process', () => ({ execSync: vi.fn() }));
|
||||
@@ -45,6 +49,19 @@ describe('GitRootDirWidget', () => {
|
||||
expect(render({ isPreview: true })).toBe('my-repo');
|
||||
});
|
||||
|
||||
it('should render preview for vscode IDE links', () => {
|
||||
const widget = new GitRootDirWidget();
|
||||
|
||||
expect(widget.render({
|
||||
id: 'git-root-dir',
|
||||
type: 'git-root-dir',
|
||||
metadata: { linkToIDE: 'vscode' }
|
||||
}, { isPreview: true }, DEFAULT_SETTINGS)).toBe(renderOsc8Link(
|
||||
buildIdeFileUrl('/Users/example/my-repo', 'vscode'),
|
||||
'my-repo'
|
||||
));
|
||||
});
|
||||
|
||||
it('should render root directory name', () => {
|
||||
mockExecSync.mockReturnValueOnce('true\n');
|
||||
mockExecSync.mockReturnValueOnce('/some/path/my-repo');
|
||||
@@ -83,6 +100,36 @@ describe('GitRootDirWidget', () => {
|
||||
expect(render()).toBe('C:');
|
||||
});
|
||||
|
||||
it('should render encoded vscode IDE links for repository roots', () => {
|
||||
const widget = new GitRootDirWidget();
|
||||
mockExecSync.mockReturnValueOnce('true\n');
|
||||
mockExecSync.mockReturnValueOnce('C:/Work/my repo#1');
|
||||
|
||||
expect(widget.render({
|
||||
id: 'git-root-dir',
|
||||
type: 'git-root-dir',
|
||||
metadata: { linkToIDE: 'vscode' }
|
||||
}, {}, DEFAULT_SETTINGS)).toBe(renderOsc8Link(
|
||||
buildIdeFileUrl('C:/Work/my repo#1', 'vscode'),
|
||||
'my repo#1'
|
||||
));
|
||||
});
|
||||
|
||||
it('should continue honoring legacy cursor link metadata', () => {
|
||||
const widget = new GitRootDirWidget();
|
||||
mockExecSync.mockReturnValueOnce('true\n');
|
||||
mockExecSync.mockReturnValueOnce('/some/path/my repo#1');
|
||||
|
||||
expect(widget.render({
|
||||
id: 'git-root-dir',
|
||||
type: 'git-root-dir',
|
||||
metadata: { linkToCursor: 'true' }
|
||||
}, {}, DEFAULT_SETTINGS)).toBe(renderOsc8Link(
|
||||
buildIdeFileUrl('/some/path/my repo#1', 'cursor'),
|
||||
'my repo#1'
|
||||
));
|
||||
});
|
||||
|
||||
it('should render no git when probe returns false', () => {
|
||||
mockExecSync.mockReturnValue('false\n');
|
||||
|
||||
@@ -101,6 +148,52 @@ describe('GitRootDirWidget', () => {
|
||||
expect(render({ hideNoGit: true })).toBeNull();
|
||||
});
|
||||
|
||||
it('should cycle IDE link modes in the editor', () => {
|
||||
const widget = new GitRootDirWidget();
|
||||
const base: WidgetItem = { id: 'git-root-dir', type: 'git-root-dir' };
|
||||
|
||||
const vscode = widget.handleEditorAction('toggle-link', base);
|
||||
const cursor = widget.handleEditorAction('toggle-link', vscode ?? base);
|
||||
const cleared = widget.handleEditorAction('toggle-link', cursor ?? base);
|
||||
|
||||
expect(vscode?.metadata?.linkToIDE).toBe('vscode');
|
||||
expect(cursor?.metadata?.linkToIDE).toBe('cursor');
|
||||
expect(cleared?.metadata?.linkToIDE).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should migrate legacy cursor metadata when cycling IDE link modes', () => {
|
||||
const widget = new GitRootDirWidget();
|
||||
const updated = widget.handleEditorAction('toggle-link', {
|
||||
id: 'git-root-dir',
|
||||
type: 'git-root-dir',
|
||||
metadata: { linkToCursor: 'true' }
|
||||
});
|
||||
|
||||
expect(updated?.metadata?.linkToCursor).toBeUndefined();
|
||||
expect(updated?.metadata?.linkToIDE).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should show IDE link modifier text in the editor display', () => {
|
||||
const widget = new GitRootDirWidget();
|
||||
const display = widget.getEditorDisplay({
|
||||
id: 'git-root-dir',
|
||||
type: 'git-root-dir',
|
||||
metadata: { linkToIDE: 'cursor' }
|
||||
});
|
||||
|
||||
expect(display.modifierText).toBe('(link-cursor)');
|
||||
});
|
||||
|
||||
it('should expose IDE link keybind', () => {
|
||||
const widget = new GitRootDirWidget();
|
||||
|
||||
expect(widget.getCustomKeybinds()).toContainEqual({
|
||||
key: 'l',
|
||||
label: '(l)ink to IDE',
|
||||
action: 'toggle-link'
|
||||
});
|
||||
});
|
||||
|
||||
it('should disable raw value support', () => {
|
||||
const widget = new GitRootDirWidget();
|
||||
|
||||
|
||||
@@ -32,9 +32,9 @@ const cases: { name: string; itemType: string; widget: GitWidget }[] = [
|
||||
|
||||
describe('Git widget shared behavior', () => {
|
||||
it.each(cases)('$name should expose hide-no-git keybind', ({ widget }) => {
|
||||
expect(widget.getCustomKeybinds()).toEqual([
|
||||
expect(widget.getCustomKeybinds()).toContainEqual(
|
||||
{ key: 'h', label: '(h)ide \'no git\' message', action: 'toggle-nogit' }
|
||||
]);
|
||||
);
|
||||
});
|
||||
|
||||
it.each(cases)('$name should toggle hideNoGit metadata', ({ widget, itemType }) => {
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
import {
|
||||
describe,
|
||||
expect,
|
||||
it
|
||||
} from 'vitest';
|
||||
|
||||
import type {
|
||||
RenderContext,
|
||||
WidgetItem
|
||||
} from '../../types';
|
||||
import { DEFAULT_SETTINGS } from '../../types/Settings';
|
||||
import { SkillsWidget } from '../Skills';
|
||||
|
||||
function render(item: WidgetItem, context: RenderContext): string | null {
|
||||
return new SkillsWidget().render(item, context, DEFAULT_SETTINGS);
|
||||
}
|
||||
|
||||
describe('SkillsWidget', () => {
|
||||
it('uses v as the mode toggle keybind', () => {
|
||||
const widget = new SkillsWidget();
|
||||
expect(widget.getCustomKeybinds({ id: 'skills', type: 'skills' })).toEqual([
|
||||
{ key: 'v', label: '(v)iew: last/count/list', action: 'cycle-mode' },
|
||||
{ key: 'h', label: '(h)ide when empty', action: 'toggle-hide-empty' }
|
||||
]);
|
||||
expect(widget.getCustomKeybinds({
|
||||
id: 'skills',
|
||||
type: 'skills',
|
||||
metadata: { mode: 'list' }
|
||||
})).toEqual([
|
||||
{ key: 'v', label: '(v)iew: last/count/list', action: 'cycle-mode' },
|
||||
{ key: 'h', label: '(h)ide when empty', action: 'toggle-hide-empty' },
|
||||
{ key: 'l', label: '(l)imit', action: 'edit-list-limit' }
|
||||
]);
|
||||
});
|
||||
|
||||
it('cycles mode current -> count -> list -> current', () => {
|
||||
const widget = new SkillsWidget();
|
||||
const base: WidgetItem = { id: 'skills', type: 'skills' };
|
||||
const count = widget.handleEditorAction('cycle-mode', base);
|
||||
const list = widget.handleEditorAction('cycle-mode', count ?? base);
|
||||
const current = widget.handleEditorAction('cycle-mode', list ?? base);
|
||||
|
||||
expect(count?.metadata?.mode).toBe('count');
|
||||
expect(list?.metadata?.mode).toBe('list');
|
||||
expect(current?.metadata?.mode).toBe('current');
|
||||
});
|
||||
|
||||
it('clears list limit metadata when leaving list mode', () => {
|
||||
const widget = new SkillsWidget();
|
||||
const updated = widget.handleEditorAction('cycle-mode', {
|
||||
id: 'skills',
|
||||
type: 'skills',
|
||||
metadata: {
|
||||
mode: 'list',
|
||||
listLimit: '2'
|
||||
}
|
||||
});
|
||||
|
||||
expect(updated?.metadata?.mode).toBe('current');
|
||||
expect(updated?.metadata?.listLimit).toBeUndefined();
|
||||
});
|
||||
|
||||
it('toggles hide-when-empty metadata', () => {
|
||||
const widget = new SkillsWidget();
|
||||
const base: WidgetItem = { id: 'skills', type: 'skills' };
|
||||
const hidden = widget.handleEditorAction('toggle-hide-empty', base);
|
||||
const shown = widget.handleEditorAction('toggle-hide-empty', hidden ?? base);
|
||||
|
||||
expect(hidden?.metadata?.hideWhenEmpty).toBe('true');
|
||||
expect(shown?.metadata?.hideWhenEmpty).toBe('false');
|
||||
});
|
||||
|
||||
it('shows hide-when-empty in editor modifier text when enabled', () => {
|
||||
const widget = new SkillsWidget();
|
||||
const display = widget.getEditorDisplay({
|
||||
id: 'skills',
|
||||
type: 'skills',
|
||||
metadata: { hideWhenEmpty: 'true' }
|
||||
});
|
||||
|
||||
expect(display.modifierText).toBe('(last used, hide when empty)');
|
||||
});
|
||||
|
||||
it('shows list limit in editor modifier text when configured', () => {
|
||||
const widget = new SkillsWidget();
|
||||
const display = widget.getEditorDisplay({
|
||||
id: 'skills',
|
||||
type: 'skills',
|
||||
metadata: { mode: 'list', listLimit: '2' }
|
||||
});
|
||||
|
||||
expect(display.modifierText).toBe('(unique list, limit: 2)');
|
||||
});
|
||||
|
||||
it('renders current, count, and list modes from skills metrics', () => {
|
||||
const context: RenderContext = {
|
||||
skillsMetrics: {
|
||||
totalInvocations: 3,
|
||||
uniqueSkills: ['commit', 'review-pr'],
|
||||
lastSkill: 'review-pr'
|
||||
}
|
||||
};
|
||||
|
||||
expect(render({ id: 'skills', type: 'skills' }, context)).toBe('Skill: review-pr');
|
||||
expect(render({ id: 'skills', type: 'skills', metadata: { mode: 'count' } }, context)).toBe('Skills: 3');
|
||||
expect(render({ id: 'skills', type: 'skills', metadata: { mode: 'list' } }, context)).toBe('Skills: commit, review-pr');
|
||||
expect(render({ id: 'skills', type: 'skills', metadata: { mode: 'list', listLimit: '1' } }, context)).toBe('Skills: commit');
|
||||
expect(render({ id: 'skills', type: 'skills', metadata: { mode: 'list', listLimit: '0' } }, context)).toBe('Skills: commit, review-pr');
|
||||
});
|
||||
|
||||
it('shows non-hidden empty outputs by default', () => {
|
||||
const context: RenderContext = {
|
||||
skillsMetrics: {
|
||||
totalInvocations: 0,
|
||||
uniqueSkills: [],
|
||||
lastSkill: null
|
||||
}
|
||||
};
|
||||
|
||||
expect(render({ id: 'skills', type: 'skills' }, context)).toBe('Skill: none');
|
||||
expect(render({ id: 'skills', type: 'skills', metadata: { mode: 'count' } }, context)).toBe('Skills: 0');
|
||||
expect(render({ id: 'skills', type: 'skills', metadata: { mode: 'list' } }, context)).toBe('Skills: none');
|
||||
});
|
||||
|
||||
it('hides empty outputs when hide-when-empty is enabled', () => {
|
||||
const context: RenderContext = {
|
||||
skillsMetrics: {
|
||||
totalInvocations: 0,
|
||||
uniqueSkills: [],
|
||||
lastSkill: null
|
||||
}
|
||||
};
|
||||
|
||||
expect(render({
|
||||
id: 'skills',
|
||||
type: 'skills',
|
||||
metadata: { hideWhenEmpty: 'true' }
|
||||
}, context)).toBeNull();
|
||||
expect(render({
|
||||
id: 'skills',
|
||||
type: 'skills',
|
||||
metadata: { mode: 'count', hideWhenEmpty: 'true' }
|
||||
}, context)).toBeNull();
|
||||
expect(render({
|
||||
id: 'skills',
|
||||
type: 'skills',
|
||||
metadata: { mode: 'list', hideWhenEmpty: 'true' }
|
||||
}, context)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,209 @@
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import type { Mock } from 'vitest';
|
||||
import {
|
||||
afterEach,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi
|
||||
} from 'vitest';
|
||||
|
||||
import type {
|
||||
RenderContext,
|
||||
WidgetItem
|
||||
} from '../../types';
|
||||
import { DEFAULT_SETTINGS } from '../../types/Settings';
|
||||
import { loadClaudeSettingsSync } from '../../utils/claude-settings';
|
||||
import { ThinkingEffortWidget } from '../ThinkingEffort';
|
||||
|
||||
// Mock claude-settings to avoid filesystem reads in tests
|
||||
vi.mock('../../utils/claude-settings', () => ({ loadClaudeSettingsSync: vi.fn() }));
|
||||
|
||||
const mockedLoadSettings = loadClaudeSettingsSync as Mock;
|
||||
const MODEL_WITH_HIGH_EFFORT = '<local-command-stdout>Set model to \u001b[1mopus (claude-opus-4-6)\u001b[22m with \u001b[1mhigh\u001b[22m effort</local-command-stdout>';
|
||||
const MODEL_WITH_LOW_EFFORT = '<local-command-stdout>Set model to \u001b[1msonnet (claude-sonnet-4-5)\u001b[22m with \u001b[1mlow\u001b[22m effort</local-command-stdout>';
|
||||
const MODEL_WITH_MAX_EFFORT = '<local-command-stdout>Set model to \u001b[1mopus (claude-opus-4-6)\u001b[22m with \u001b[1mmax\u001b[22m effort</local-command-stdout>';
|
||||
const MODEL_WITHOUT_EFFORT = '<local-command-stdout>Set model to \u001b[1msonnet (claude-sonnet-4-5)\u001b[22m</local-command-stdout>';
|
||||
|
||||
let tempDir: string;
|
||||
|
||||
function makeTranscriptEntry(content: string): string {
|
||||
return JSON.stringify({
|
||||
type: 'user',
|
||||
message: {
|
||||
role: 'user',
|
||||
content
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function render(options: {
|
||||
transcriptPath?: string;
|
||||
fileContent?: string | null | undefined;
|
||||
rawValue?: boolean;
|
||||
isPreview?: boolean;
|
||||
settingsValue?: unknown;
|
||||
} = {}): string | null {
|
||||
const {
|
||||
transcriptPath = options.fileContent !== undefined ? path.join(tempDir, 'session.jsonl') : undefined,
|
||||
fileContent,
|
||||
rawValue = false,
|
||||
isPreview = false,
|
||||
settingsValue = {}
|
||||
} = options;
|
||||
|
||||
const widget = new ThinkingEffortWidget();
|
||||
const context: RenderContext = {
|
||||
data: transcriptPath ? { transcript_path: transcriptPath } : undefined,
|
||||
isPreview
|
||||
};
|
||||
const item: WidgetItem = {
|
||||
id: 'thinking-effort',
|
||||
type: 'thinking-effort',
|
||||
rawValue
|
||||
};
|
||||
|
||||
mockedLoadSettings.mockReturnValue(settingsValue as never);
|
||||
|
||||
if (transcriptPath && fileContent !== undefined && fileContent !== null) {
|
||||
fs.writeFileSync(transcriptPath, fileContent, 'utf-8');
|
||||
}
|
||||
|
||||
return widget.render(item, context, DEFAULT_SETTINGS);
|
||||
}
|
||||
|
||||
describe('ThinkingEffortWidget', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccstatusline-thinking-effort-'));
|
||||
mockedLoadSettings.mockReturnValue({} as never);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('metadata', () => {
|
||||
it('has correct display name', () => {
|
||||
const widget = new ThinkingEffortWidget();
|
||||
expect(widget.getDisplayName()).toBe('Thinking Effort');
|
||||
});
|
||||
|
||||
it('has correct category', () => {
|
||||
const widget = new ThinkingEffortWidget();
|
||||
expect(widget.getCategory()).toBe('Core');
|
||||
});
|
||||
|
||||
it('supports raw value', () => {
|
||||
const widget = new ThinkingEffortWidget();
|
||||
expect(widget.supportsRawValue()).toBe(true);
|
||||
});
|
||||
|
||||
it('supports colors', () => {
|
||||
const widget = new ThinkingEffortWidget();
|
||||
expect(widget.supportsColors({ type: 'thinking-effort' } as never)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('preview mode', () => {
|
||||
it('returns labelled preview', () => {
|
||||
const result = render({ isPreview: true });
|
||||
expect(result).toBe('Thinking: high');
|
||||
});
|
||||
|
||||
it('returns raw preview', () => {
|
||||
const result = render({ isPreview: true, rawValue: true });
|
||||
expect(result).toBe('high');
|
||||
});
|
||||
});
|
||||
|
||||
describe('transcript source', () => {
|
||||
it('reads effort from the latest /model transcript stdout', () => {
|
||||
const result = render({
|
||||
fileContent: makeTranscriptEntry(MODEL_WITH_HIGH_EFFORT),
|
||||
settingsValue: { effortLevel: 'low' }
|
||||
});
|
||||
expect(result).toBe('Thinking: high');
|
||||
});
|
||||
|
||||
it('returns raw transcript effort when requested', () => {
|
||||
const result = render({
|
||||
fileContent: makeTranscriptEntry(MODEL_WITH_LOW_EFFORT),
|
||||
rawValue: true
|
||||
});
|
||||
expect(result).toBe('low');
|
||||
});
|
||||
|
||||
it('supports max effort from transcript output', () => {
|
||||
const result = render({ fileContent: makeTranscriptEntry(MODEL_WITH_MAX_EFFORT) });
|
||||
expect(result).toBe('Thinking: max');
|
||||
});
|
||||
|
||||
it('does not keep stale transcript effort when a newer /model output has no effort', () => {
|
||||
const result = render({
|
||||
fileContent: [
|
||||
makeTranscriptEntry(MODEL_WITH_HIGH_EFFORT),
|
||||
makeTranscriptEntry('<local-command-stdout>Bye!</local-command-stdout>'),
|
||||
makeTranscriptEntry(MODEL_WITHOUT_EFFORT)
|
||||
].join('\n'),
|
||||
settingsValue: { effortLevel: 'medium' }
|
||||
});
|
||||
expect(result).toBe('Thinking: medium');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Claude settings fallback', () => {
|
||||
it('falls back to effortLevel when the latest /model output has no effort', () => {
|
||||
const result = render({
|
||||
fileContent: makeTranscriptEntry(MODEL_WITHOUT_EFFORT),
|
||||
settingsValue: { effortLevel: 'high' }
|
||||
});
|
||||
expect(result).toBe('Thinking: high');
|
||||
});
|
||||
|
||||
it('falls back to effortLevel when the transcript is unavailable', () => {
|
||||
const result = render({
|
||||
transcriptPath: path.join(tempDir, 'missing.jsonl'),
|
||||
fileContent: null,
|
||||
settingsValue: { effortLevel: 'high' }
|
||||
});
|
||||
expect(result).toBe('Thinking: high');
|
||||
});
|
||||
|
||||
it('handles case-insensitive effortLevel', () => {
|
||||
const result = render({ settingsValue: { effortLevel: 'HIGH' } });
|
||||
expect(result).toBe('Thinking: high');
|
||||
});
|
||||
|
||||
it('supports max effortLevel', () => {
|
||||
const result = render({ settingsValue: { effortLevel: 'max' } });
|
||||
expect(result).toBe('Thinking: max');
|
||||
});
|
||||
|
||||
it('defaults to medium when effortLevel is not set', () => {
|
||||
const result = render();
|
||||
expect(result).toBe('Thinking: medium');
|
||||
});
|
||||
|
||||
it('defaults to medium when effortLevel is invalid', () => {
|
||||
const result = render({ settingsValue: { effortLevel: 'ultra' } });
|
||||
expect(result).toBe('Thinking: medium');
|
||||
});
|
||||
|
||||
it('defaults to medium when settings read fails', () => {
|
||||
mockedLoadSettings.mockImplementation(() => {
|
||||
throw new Error('settings unavailable');
|
||||
});
|
||||
const result = render();
|
||||
expect(result).toBe('Thinking: medium');
|
||||
});
|
||||
|
||||
it('defaults to medium when the latest /model output has no effort and settings are missing', () => {
|
||||
const result = render({ fileContent: makeTranscriptEntry(MODEL_WITHOUT_EFFORT) });
|
||||
expect(result).toBe('Thinking: medium');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,147 @@
|
||||
import {
|
||||
describe,
|
||||
expect,
|
||||
it
|
||||
} from 'vitest';
|
||||
|
||||
import type {
|
||||
RenderContext,
|
||||
WidgetItem
|
||||
} from '../../types';
|
||||
import { DEFAULT_SETTINGS } from '../../types/Settings';
|
||||
import { VimModeWidget } from '../VimMode';
|
||||
|
||||
const ITEM: WidgetItem = { id: 'vim-mode', type: 'vim-mode' };
|
||||
|
||||
function makeContext(overrides: Partial<RenderContext> = {}): RenderContext {
|
||||
return { ...overrides };
|
||||
}
|
||||
|
||||
describe('VimModeWidget', () => {
|
||||
describe('editor configuration', () => {
|
||||
it('uses f as the format toggle keybind', () => {
|
||||
expect(new VimModeWidget().getCustomKeybinds()).toEqual([
|
||||
{ key: 'f', label: '(f)ormat', action: 'cycle-format' },
|
||||
{ key: 'n', label: '(n)erd font', action: 'toggle-nerd-font' }
|
||||
]);
|
||||
});
|
||||
|
||||
it('defaults to icon-dash-letter in the editor display', () => {
|
||||
expect(new VimModeWidget().getEditorDisplay(ITEM)).toEqual({
|
||||
displayText: 'Vim Mode',
|
||||
modifierText: '(icon-dash-letter)'
|
||||
});
|
||||
});
|
||||
|
||||
it('shows nerd font in the editor display when enabled', () => {
|
||||
expect(new VimModeWidget().getEditorDisplay({
|
||||
...ITEM,
|
||||
metadata: { nerdFont: 'true' }
|
||||
})).toEqual({
|
||||
displayText: 'Vim Mode',
|
||||
modifierText: '(icon-dash-letter, nerd font)'
|
||||
});
|
||||
});
|
||||
|
||||
it('cycles icon-dash-letter -> icon-letter -> icon -> letter -> word -> icon-dash-letter', () => {
|
||||
const widget = new VimModeWidget();
|
||||
const iconLetter = widget.handleEditorAction('cycle-format', ITEM);
|
||||
const icon = widget.handleEditorAction('cycle-format', iconLetter ?? ITEM);
|
||||
const letter = widget.handleEditorAction('cycle-format', icon ?? ITEM);
|
||||
const word = widget.handleEditorAction('cycle-format', letter ?? ITEM);
|
||||
const iconDashLetter = widget.handleEditorAction('cycle-format', word ?? ITEM);
|
||||
|
||||
expect(iconLetter?.metadata?.format).toBe('icon-letter');
|
||||
expect(icon?.metadata?.format).toBe('icon');
|
||||
expect(letter?.metadata?.format).toBe('letter');
|
||||
expect(word?.metadata?.format).toBe('word');
|
||||
expect(iconDashLetter?.metadata?.format).toBeUndefined();
|
||||
});
|
||||
|
||||
it('toggles nerd font metadata on and off', () => {
|
||||
const widget = new VimModeWidget();
|
||||
const enabled = widget.handleEditorAction('toggle-nerd-font', ITEM);
|
||||
const disabled = widget.handleEditorAction('toggle-nerd-font', enabled ?? ITEM);
|
||||
|
||||
expect(enabled?.metadata?.nerdFont).toBe('true');
|
||||
expect(disabled?.metadata?.nerdFont).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('metadata', () => {
|
||||
it('has correct display name', () => {
|
||||
expect(new VimModeWidget().getDisplayName()).toBe('Vim Mode');
|
||||
});
|
||||
|
||||
it('has correct category', () => {
|
||||
expect(new VimModeWidget().getCategory()).toBe('Core');
|
||||
});
|
||||
|
||||
it('does not support raw value', () => {
|
||||
expect(new VimModeWidget().supportsRawValue()).toBe(false);
|
||||
});
|
||||
|
||||
it('supports colors', () => {
|
||||
expect(new VimModeWidget().supportsColors(ITEM)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('render()', () => {
|
||||
it('returns v-N by default', () => {
|
||||
const ctx = makeContext({ data: { vim: { mode: 'NORMAL' } } });
|
||||
expect(new VimModeWidget().render(ITEM, ctx, DEFAULT_SETTINGS)).toBe('v-N');
|
||||
});
|
||||
|
||||
it('returns v-I by default for INSERT', () => {
|
||||
const ctx = makeContext({ data: { vim: { mode: 'INSERT' } } });
|
||||
expect(new VimModeWidget().render(ITEM, ctx, DEFAULT_SETTINGS)).toBe('v-I');
|
||||
});
|
||||
|
||||
it('renders alternate configured formats', () => {
|
||||
const ctx = makeContext({ data: { vim: { mode: 'NORMAL' } } });
|
||||
|
||||
expect(new VimModeWidget().render({
|
||||
...ITEM,
|
||||
metadata: { format: 'letter' }
|
||||
}, ctx, DEFAULT_SETTINGS)).toBe('N');
|
||||
expect(new VimModeWidget().render({
|
||||
...ITEM,
|
||||
metadata: { format: 'word' }
|
||||
}, ctx, DEFAULT_SETTINGS)).toBe('NORMAL');
|
||||
});
|
||||
|
||||
it('renders the Nerd Font glyph when enabled', () => {
|
||||
const ctx = makeContext({ data: { vim: { mode: 'NORMAL' } } });
|
||||
|
||||
expect(new VimModeWidget().render({
|
||||
...ITEM,
|
||||
metadata: { nerdFont: 'true' }
|
||||
}, ctx, DEFAULT_SETTINGS)).toBe('\uE62B-N');
|
||||
});
|
||||
|
||||
it('returns null when vim field is absent (vim disabled)', () => {
|
||||
const ctx = makeContext({ data: { model: { id: 'claude-sonnet-4-5' } } });
|
||||
expect(new VimModeWidget().render(ITEM, ctx, DEFAULT_SETTINGS)).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null when vim field is null', () => {
|
||||
const ctx = makeContext({ data: { vim: null } });
|
||||
expect(new VimModeWidget().render(ITEM, ctx, DEFAULT_SETTINGS)).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null when context.data is absent', () => {
|
||||
const ctx = makeContext();
|
||||
expect(new VimModeWidget().render(ITEM, ctx, DEFAULT_SETTINGS)).toBeNull();
|
||||
});
|
||||
|
||||
it('returns v-N in preview mode regardless of data', () => {
|
||||
const ctx = makeContext({ isPreview: true });
|
||||
expect(new VimModeWidget().render(ITEM, ctx, DEFAULT_SETTINGS)).toBe('v-N');
|
||||
});
|
||||
|
||||
it('returns v-C for unknown modes by default', () => {
|
||||
const ctx = makeContext({ data: { vim: { mode: 'COMMAND' } } });
|
||||
expect(new VimModeWidget().render(ITEM, ctx, DEFAULT_SETTINGS)).toBe('v-C');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -39,7 +39,17 @@ describe('WeeklyResetTimerWidget', () => {
|
||||
it('renders preview using weekly reset format', () => {
|
||||
const widget = new WeeklyResetTimerWidget();
|
||||
|
||||
expect(render(widget, { id: 'weekly-reset', type: 'weekly-reset-timer' }, { isPreview: true })).toBe('Weekly Reset: 36hr 30m');
|
||||
expect(render(widget, { id: 'weekly-reset', type: 'weekly-reset-timer' }, { isPreview: true })).toBe('Weekly Reset: 1d 12hr 30m');
|
||||
});
|
||||
|
||||
it('renders preview in hours-only mode when toggled', () => {
|
||||
const widget = new WeeklyResetTimerWidget();
|
||||
|
||||
expect(render(widget, {
|
||||
id: 'weekly-reset',
|
||||
type: 'weekly-reset-timer',
|
||||
metadata: { hours: 'true' }
|
||||
}, { isPreview: true })).toBe('Weekly Reset: 36hr 30m');
|
||||
});
|
||||
|
||||
it('renders remaining time in time mode', () => {
|
||||
@@ -55,6 +65,27 @@ describe('WeeklyResetTimerWidget', () => {
|
||||
mockFormatUsageDuration.mockReturnValue('134hr 40m');
|
||||
|
||||
expect(render(widget, { id: 'weekly-reset', type: 'weekly-reset-timer' }, { usageData: {} })).toBe('Weekly Reset: 134hr 40m');
|
||||
expect(mockFormatUsageDuration).toHaveBeenCalledWith(484800000, false, true);
|
||||
});
|
||||
|
||||
it('renders remaining time in hours-only mode', () => {
|
||||
const widget = new WeeklyResetTimerWidget();
|
||||
|
||||
mockResolveWeeklyUsageWindow.mockReturnValue({
|
||||
sessionDurationMs: 604800000,
|
||||
elapsedMs: 120000000,
|
||||
remainingMs: 484800000,
|
||||
elapsedPercent: 19.8412698413,
|
||||
remainingPercent: 80.1587301587
|
||||
});
|
||||
mockFormatUsageDuration.mockReturnValue('134hr 40m');
|
||||
|
||||
expect(render(widget, {
|
||||
id: 'weekly-reset',
|
||||
type: 'weekly-reset-timer',
|
||||
metadata: { hours: 'true' }
|
||||
}, { usageData: {} })).toBe('Weekly Reset: 134hr 40m');
|
||||
expect(mockFormatUsageDuration).toHaveBeenCalledWith(484800000, false, false);
|
||||
});
|
||||
|
||||
it('renders short progress bar with inverted fill', () => {
|
||||
@@ -111,10 +142,60 @@ describe('WeeklyResetTimerWidget', () => {
|
||||
expect(render(widget, { id: 'weekly-reset', type: 'weekly-reset-timer', rawValue: true }, { usageData: {} })).toBe('120hr 15m');
|
||||
});
|
||||
|
||||
it('toggles hours-only metadata and shows hours-only modifier text', () => {
|
||||
const widget = new WeeklyResetTimerWidget();
|
||||
const baseItem: WidgetItem = { id: 'weekly-reset', type: 'weekly-reset-timer' };
|
||||
|
||||
const hoursOnly = widget.handleEditorAction('toggle-hours', baseItem);
|
||||
const cleared = widget.handleEditorAction('toggle-hours', hoursOnly ?? baseItem);
|
||||
|
||||
expect(hoursOnly?.metadata?.hours).toBe('true');
|
||||
expect(cleared?.metadata?.hours).toBe('false');
|
||||
expect(widget.getEditorDisplay(baseItem).modifierText).toBeUndefined();
|
||||
expect(widget.getEditorDisplay({
|
||||
...baseItem,
|
||||
metadata: { hours: 'true' }
|
||||
}).modifierText).toBe('(hours only)');
|
||||
});
|
||||
|
||||
it('clears compact and hours-only metadata when cycling into progress mode', () => {
|
||||
const widget = new WeeklyResetTimerWidget();
|
||||
const updated = widget.handleEditorAction('toggle-progress', {
|
||||
id: 'weekly-reset',
|
||||
type: 'weekly-reset-timer',
|
||||
metadata: {
|
||||
compact: 'true',
|
||||
hours: 'true'
|
||||
}
|
||||
});
|
||||
|
||||
expect(updated?.metadata?.display).toBe('progress');
|
||||
expect(updated?.metadata?.compact).toBeUndefined();
|
||||
expect(updated?.metadata?.hours).toBeUndefined();
|
||||
});
|
||||
|
||||
it('ignores stale hours-only metadata in progress mode editor modifiers', () => {
|
||||
const widget = new WeeklyResetTimerWidget();
|
||||
|
||||
expect(widget.getEditorDisplay({
|
||||
id: 'weekly-reset',
|
||||
type: 'weekly-reset-timer',
|
||||
metadata: {
|
||||
display: 'progress',
|
||||
hours: 'true'
|
||||
}
|
||||
}).modifierText).toBe('(progress bar)');
|
||||
});
|
||||
|
||||
runUsageTimerEditorSuite({
|
||||
baseItem: { id: 'weekly-reset', type: 'weekly-reset-timer' },
|
||||
createWidget: () => new WeeklyResetTimerWidget(),
|
||||
expectedDisplayName: 'Weekly Reset Timer',
|
||||
expectedTimeKeybinds: [
|
||||
{ key: 'p', label: '(p)rogress toggle', action: 'toggle-progress' },
|
||||
{ key: 's', label: '(s)hort time', action: 'toggle-compact' },
|
||||
{ key: 'h', label: '(h)ours only', action: 'toggle-hours' }
|
||||
],
|
||||
expectedModifierText: '(short bar, inverted)',
|
||||
modifierItem: {
|
||||
id: 'weekly-reset',
|
||||
|
||||
@@ -13,7 +13,7 @@ import type {
|
||||
} from '../../../types/Widget';
|
||||
|
||||
interface UsageWidgetLike {
|
||||
getCustomKeybinds(): CustomKeybind[];
|
||||
getCustomKeybinds(item?: WidgetItem): CustomKeybind[];
|
||||
getEditorDisplay(item: WidgetItem): WidgetEditorDisplay;
|
||||
handleEditorAction(action: string, item: WidgetItem): WidgetItem | null;
|
||||
supportsRawValue(): boolean;
|
||||
@@ -41,11 +41,27 @@ interface UsageTimerEditorSuiteConfig<TWidget extends UsageWidgetLike & { getDis
|
||||
baseItem: WidgetItem;
|
||||
createWidget: () => TWidget;
|
||||
expectedDisplayName: string;
|
||||
expectedProgressKeybinds?: CustomKeybind[];
|
||||
expectedModifierText: string;
|
||||
modifierItem: WidgetItem;
|
||||
expectedTimeKeybinds?: CustomKeybind[];
|
||||
}
|
||||
|
||||
const EXPECTED_USAGE_KEYBINDS: CustomKeybind[] = [
|
||||
{ key: 'p', label: '(p)rogress toggle', action: 'toggle-progress' }
|
||||
];
|
||||
|
||||
const EXPECTED_USAGE_PROGRESS_KEYBINDS: CustomKeybind[] = [
|
||||
{ key: 'p', label: '(p)rogress toggle', action: 'toggle-progress' },
|
||||
{ key: 'v', label: 'in(v)ert fill', action: 'toggle-invert' }
|
||||
];
|
||||
|
||||
const EXPECTED_TIMER_TIME_KEYBINDS: CustomKeybind[] = [
|
||||
{ key: 'p', label: '(p)rogress toggle', action: 'toggle-progress' },
|
||||
{ key: 's', label: '(s)hort time', action: 'toggle-compact' }
|
||||
];
|
||||
|
||||
const EXPECTED_TIMER_PROGRESS_KEYBINDS: CustomKeybind[] = [
|
||||
{ key: 'p', label: '(p)rogress toggle', action: 'toggle-progress' },
|
||||
{ key: 'v', label: 'in(v)ert fill', action: 'toggle-invert' }
|
||||
];
|
||||
@@ -61,11 +77,12 @@ export function runUsagePercentWidgetSuite<TWidget extends UsageWidgetLike>(conf
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('exposes progress and invert keybinds', () => {
|
||||
it('exposes widget-managed keybinds for time and progress modes', () => {
|
||||
const widget = config.createWidget();
|
||||
|
||||
expect(widget.supportsRawValue()).toBe(true);
|
||||
expect(widget.getCustomKeybinds()).toEqual(EXPECTED_USAGE_KEYBINDS);
|
||||
expect(widget.getCustomKeybinds(config.baseItem)).toEqual(EXPECTED_USAGE_KEYBINDS);
|
||||
expect(widget.getCustomKeybinds(config.progressItem)).toEqual(EXPECTED_USAGE_PROGRESS_KEYBINDS);
|
||||
});
|
||||
|
||||
it.each([
|
||||
@@ -140,6 +157,23 @@ export function runUsagePercentWidgetSuite<TWidget extends UsageWidgetLike>(conf
|
||||
expect(widget.getEditorDisplay(config.baseItem).modifierText).toBeUndefined();
|
||||
expect(widget.getEditorDisplay(config.modifierItem).modifierText).toBe(config.expectedModifierText);
|
||||
});
|
||||
|
||||
it('ignores stale compact metadata in editor modifiers', () => {
|
||||
const widget = config.createWidget();
|
||||
const modifierItemWithCompact: WidgetItem = {
|
||||
...config.modifierItem,
|
||||
metadata: {
|
||||
...(config.modifierItem.metadata ?? {}),
|
||||
compact: 'true'
|
||||
}
|
||||
};
|
||||
|
||||
expect(widget.getEditorDisplay({
|
||||
...config.baseItem,
|
||||
metadata: { compact: 'true' }
|
||||
}).modifierText).toBeUndefined();
|
||||
expect(widget.getEditorDisplay(modifierItemWithCompact).modifierText).toBe(config.expectedModifierText);
|
||||
});
|
||||
}
|
||||
|
||||
export function runUsageTimerEditorSuite<TWidget extends UsageWidgetLike & { getDisplayName(): string }>(config: UsageTimerEditorSuiteConfig<TWidget>): void {
|
||||
@@ -147,12 +181,13 @@ export function runUsageTimerEditorSuite<TWidget extends UsageWidgetLike & { get
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('supports raw value and exposes progress/invert keybinds', () => {
|
||||
it('supports raw value and exposes widget-managed keybinds for time and progress modes', () => {
|
||||
const widget = config.createWidget();
|
||||
|
||||
expect(widget.getDisplayName()).toBe(config.expectedDisplayName);
|
||||
expect(widget.supportsRawValue()).toBe(true);
|
||||
expect(widget.getCustomKeybinds()).toEqual(EXPECTED_USAGE_KEYBINDS);
|
||||
expect(widget.getCustomKeybinds(config.baseItem)).toEqual(config.expectedTimeKeybinds ?? EXPECTED_TIMER_TIME_KEYBINDS);
|
||||
expect(widget.getCustomKeybinds(config.modifierItem)).toEqual(config.expectedProgressKeybinds ?? EXPECTED_TIMER_PROGRESS_KEYBINDS);
|
||||
});
|
||||
|
||||
it('clears invert metadata when cycling back to time mode', () => {
|
||||
@@ -181,6 +216,17 @@ export function runUsageTimerEditorSuite<TWidget extends UsageWidgetLike & { get
|
||||
expect(third?.metadata?.display).toBe('time');
|
||||
});
|
||||
|
||||
it('clears compact metadata when cycling into progress mode', () => {
|
||||
const widget = config.createWidget();
|
||||
const updated = widget.handleEditorAction('toggle-progress', {
|
||||
...config.baseItem,
|
||||
metadata: { compact: 'true' }
|
||||
});
|
||||
|
||||
expect(updated?.metadata?.display).toBe('progress');
|
||||
expect(updated?.metadata?.compact).toBeUndefined();
|
||||
});
|
||||
|
||||
it('toggles invert metadata and shows editor modifiers', () => {
|
||||
const widget = config.createWidget();
|
||||
|
||||
@@ -192,4 +238,15 @@ export function runUsageTimerEditorSuite<TWidget extends UsageWidgetLike & { get
|
||||
expect(widget.getEditorDisplay(config.baseItem).modifierText).toBeUndefined();
|
||||
expect(widget.getEditorDisplay(config.modifierItem).modifierText).toBe(config.expectedModifierText);
|
||||
});
|
||||
|
||||
it('toggles compact metadata and shows compact modifier text', () => {
|
||||
const widget = config.createWidget();
|
||||
|
||||
const compact = widget.handleEditorAction('toggle-compact', config.baseItem);
|
||||
const cleared = widget.handleEditorAction('toggle-compact', compact ?? config.baseItem);
|
||||
|
||||
expect(compact?.metadata?.compact).toBe('true');
|
||||
expect(cleared?.metadata?.compact).toBe('false');
|
||||
expect(widget.getEditorDisplay({ ...config.baseItem, metadata: { compact: 'true' } }).modifierText).toBe('(compact)');
|
||||
});
|
||||
}
|
||||
@@ -32,4 +32,7 @@ export { WeeklyUsageWidget } from './WeeklyUsage';
|
||||
export { BlockResetTimerWidget } from './BlockResetTimer';
|
||||
export { WeeklyResetTimerWidget } from './WeeklyResetTimer';
|
||||
export { ContextBarWidget } from './ContextBar';
|
||||
export { LinkWidget } from './Link';
|
||||
export { LinkWidget } from './Link';
|
||||
export { SkillsWidget } from './Skills';
|
||||
export { ThinkingEffortWidget } from './ThinkingEffort';
|
||||
export { VimModeWidget } from './VimMode';
|
||||
@@ -12,4 +12,15 @@ export function toggleMetadataFlag(item: WidgetItem, key: string): WidgetItem {
|
||||
[key]: (!isMetadataFlagEnabled(item, key)).toString()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function removeMetadataKeys(item: WidgetItem, keys: string[]): WidgetItem {
|
||||
const nextMetadata = Object.fromEntries(
|
||||
Object.entries(item.metadata ?? {}).filter(([key]) => !keys.includes(key))
|
||||
);
|
||||
|
||||
return {
|
||||
...item,
|
||||
metadata: Object.keys(nextMetadata).length > 0 ? nextMetadata : undefined
|
||||
};
|
||||
}
|
||||
@@ -1,13 +1,21 @@
|
||||
import type { WidgetItem } from '../../types/Widget';
|
||||
import type {
|
||||
CustomKeybind,
|
||||
WidgetItem
|
||||
} from '../../types/Widget';
|
||||
|
||||
import { makeModifierText } from './editor-display';
|
||||
import {
|
||||
isMetadataFlagEnabled,
|
||||
removeMetadataKeys,
|
||||
toggleMetadataFlag
|
||||
} from './metadata';
|
||||
|
||||
export type UsageDisplayMode = 'time' | 'progress' | 'progress-short';
|
||||
|
||||
const PROGRESS_TOGGLE_KEYBIND: CustomKeybind = { key: 'p', label: '(p)rogress toggle', action: 'toggle-progress' };
|
||||
const INVERT_TOGGLE_KEYBIND: CustomKeybind = { key: 'v', label: 'in(v)ert fill', action: 'toggle-invert' };
|
||||
const COMPACT_TOGGLE_KEYBIND: CustomKeybind = { key: 's', label: '(s)hort time', action: 'toggle-compact' };
|
||||
|
||||
export function getUsageDisplayMode(item: WidgetItem): UsageDisplayMode {
|
||||
const mode = item.metadata?.display;
|
||||
if (mode === 'progress' || mode === 'progress-short') {
|
||||
@@ -28,7 +36,20 @@ export function isUsageInverted(item: WidgetItem): boolean {
|
||||
return isMetadataFlagEnabled(item, 'invert');
|
||||
}
|
||||
|
||||
export function getUsageDisplayModifierText(item: WidgetItem): string | undefined {
|
||||
export function isUsageCompact(item: WidgetItem): boolean {
|
||||
return isMetadataFlagEnabled(item, 'compact');
|
||||
}
|
||||
|
||||
export function toggleUsageCompact(item: WidgetItem): WidgetItem {
|
||||
return toggleMetadataFlag(item, 'compact');
|
||||
}
|
||||
|
||||
interface UsageDisplayModifierOptions { includeCompact?: boolean }
|
||||
|
||||
export function getUsageDisplayModifierText(
|
||||
item: WidgetItem,
|
||||
options: UsageDisplayModifierOptions = {}
|
||||
): string | undefined {
|
||||
const mode = getUsageDisplayMode(item);
|
||||
const modifiers: string[] = [];
|
||||
|
||||
@@ -42,10 +63,14 @@ export function getUsageDisplayModifierText(item: WidgetItem): string | undefine
|
||||
modifiers.push('inverted');
|
||||
}
|
||||
|
||||
if (options.includeCompact && !isUsageProgressMode(mode) && isUsageCompact(item)) {
|
||||
modifiers.push('compact');
|
||||
}
|
||||
|
||||
return makeModifierText(modifiers);
|
||||
}
|
||||
|
||||
export function cycleUsageDisplayMode(item: WidgetItem): WidgetItem {
|
||||
export function cycleUsageDisplayMode(item: WidgetItem, disabledInProgressKeys: string[] = []): WidgetItem {
|
||||
const currentMode = getUsageDisplayMode(item);
|
||||
const nextMode: UsageDisplayMode = currentMode === 'time'
|
||||
? 'progress'
|
||||
@@ -53,21 +78,42 @@ export function cycleUsageDisplayMode(item: WidgetItem): WidgetItem {
|
||||
? 'progress-short'
|
||||
: 'time';
|
||||
|
||||
const nextItem = removeMetadataKeys(item, nextMode === 'time'
|
||||
? ['invert']
|
||||
: disabledInProgressKeys);
|
||||
const nextMetadata: Record<string, string> = {
|
||||
...(item.metadata ?? {}),
|
||||
...(nextItem.metadata ?? {}),
|
||||
display: nextMode
|
||||
};
|
||||
|
||||
if (nextMode === 'time') {
|
||||
delete nextMetadata.invert;
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
...nextItem,
|
||||
metadata: nextMetadata
|
||||
};
|
||||
}
|
||||
|
||||
export function toggleUsageInverted(item: WidgetItem): WidgetItem {
|
||||
return toggleMetadataFlag(item, 'invert');
|
||||
}
|
||||
|
||||
export function getUsagePercentCustomKeybinds(item?: WidgetItem): CustomKeybind[] {
|
||||
const keybinds = [PROGRESS_TOGGLE_KEYBIND];
|
||||
|
||||
if (item && isUsageProgressMode(getUsageDisplayMode(item))) {
|
||||
keybinds.push(INVERT_TOGGLE_KEYBIND);
|
||||
}
|
||||
|
||||
return keybinds;
|
||||
}
|
||||
|
||||
export function getUsageTimerCustomKeybinds(item?: WidgetItem): CustomKeybind[] {
|
||||
const keybinds = [PROGRESS_TOGGLE_KEYBIND];
|
||||
|
||||
if (item && isUsageProgressMode(getUsageDisplayMode(item))) {
|
||||
keybinds.push(INVERT_TOGGLE_KEYBIND);
|
||||
} else {
|
||||
keybinds.push(COMPACT_TOGGLE_KEYBIND);
|
||||
}
|
||||
|
||||
return keybinds;
|
||||
}
|
||||
Reference in New Issue
Block a user