├── mise.toml ├── src ├── config │ └── constants.ts ├── types │ └── send.ts ├── utils │ ├── contentProcessor.ts │ ├── argumentParser.ts │ ├── sendConfig.ts │ ├── envParser.ts │ ├── tempFile.ts │ └── quoteProcessor.ts ├── index.ts ├── modes │ ├── args.ts │ ├── dump.ts │ ├── register.ts │ ├── common.ts │ ├── resume.ts │ ├── collect.ts │ ├── openEditor.ts │ └── input.ts └── modules │ ├── editor.ts │ ├── tmux.ts │ └── wezterm.ts ├── tsdown.config.ts ├── docs ├── pseudo-ai-agent-output.txt ├── write-close-send.tape ├── write-send-without-closing.tape ├── quote-capture.tape ├── developer.md ├── migration-guide-v1.md └── modes.md ├── test ├── fixtures │ └── test-cli.ts ├── utils │ ├── tempFile.test.ts │ ├── contentProcessor.test.ts │ ├── envParser.test.ts │ ├── sendConfig.test.ts │ └── quoteProcessor.test.ts ├── integration.test.ts ├── argumentParser.test.ts └── modules │ ├── wezterm.test.ts │ └── editor.test.ts ├── biome.jsonc ├── .github └── workflows │ ├── ci.yaml │ └── publish.yaml ├── .gitignore ├── tsconfig.json ├── LICENSE ├── .claude └── commands │ └── commit.md ├── package.json ├── CLAUDE.md ├── README.md └── bun.lock /mise.toml: -------------------------------------------------------------------------------- 1 | [tools] 2 | node = "22" 3 | 4 | [env] 5 | COMMIT_MESSAGE_ENGLISH = 1 6 | -------------------------------------------------------------------------------- /src/config/constants.ts: -------------------------------------------------------------------------------- 1 | export const TEMP_FILE_PREFIX = ".editprompt-"; 2 | export const TEMP_FILE_EXTENSION = ".md"; 3 | export const DEFAULT_EDITOR = "vim"; 4 | -------------------------------------------------------------------------------- /src/types/send.ts: -------------------------------------------------------------------------------- 1 | import type { MuxType } from "../modes/common"; 2 | 3 | export interface SendConfig { 4 | mux: MuxType; 5 | alwaysCopy: boolean; 6 | } 7 | -------------------------------------------------------------------------------- /src/utils/contentProcessor.ts: -------------------------------------------------------------------------------- 1 | export function processContent(content: string): string { 2 | let processed = content.replace(/\n$/, ""); 3 | if (/@[^\n]*$/.test(processed)) { 4 | processed += " "; 5 | } 6 | return processed; 7 | } 8 | -------------------------------------------------------------------------------- /tsdown.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsdown"; 2 | 3 | export default defineConfig({ 4 | entry: ["./src/index.ts"], 5 | format: "esm", 6 | platform: "node", 7 | fixedExtension: false, 8 | dts: { 9 | oxc: true, 10 | }, 11 | }); 12 | -------------------------------------------------------------------------------- /docs/pseudo-ai-agent-output.txt: -------------------------------------------------------------------------------- 1 | Some AI agents include leading spaces in their output, 2 | which can make the copied text look a bit awkward. 3 | 4 | ---meaningless sentence--- 5 | 6 | Using editprompt’s quote mode or capture mode makes 7 | it easy to reply while quoting the AI agent’s output. 8 | -------------------------------------------------------------------------------- /test/fixtures/test-cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { cli } from "gunshi"; 3 | import { extractRawContent } from "../../src/utils/argumentParser"; 4 | 5 | const argv = process.argv.slice(2); 6 | 7 | await cli(argv, { 8 | name: "test-cli", 9 | description: "Test CLI for argument parsing", 10 | async run(ctx) { 11 | const result = extractRawContent(ctx.rest, ctx.positionals); 12 | console.log(result); 13 | }, 14 | }); 15 | -------------------------------------------------------------------------------- /src/utils/argumentParser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Extract raw content from CLI arguments 3 | * Prioritizes ctx.rest (for -- separator) over ctx.positionals 4 | * 5 | * @param rest - Arguments passed after -- separator 6 | * @param positionals - Positional arguments 7 | * @returns Raw content string or undefined if no content provided 8 | */ 9 | export function extractRawContent( 10 | rest: string[], 11 | positionals: string[], 12 | ): string | undefined { 13 | if (rest.length > 0) { 14 | return rest.join(" "); 15 | } 16 | return positionals[0]; 17 | } 18 | -------------------------------------------------------------------------------- /test/utils/tempFile.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach, describe, expect, mock, test } from "bun:test"; 2 | 3 | describe("TempFile Utility", () => { 4 | beforeEach(() => { 5 | mock.restore(); 6 | }); 7 | 8 | describe("createTempFile", () => { 9 | test("should handle basic functionality", async () => { 10 | // Simple functional test without complex mocking 11 | const { createTempFile } = await import("../../src/utils/tempFile"); 12 | 13 | const result = await createTempFile(); 14 | expect(result).toBeDefined(); 15 | expect(typeof result).toBe("string"); 16 | }); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /biome.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "vcs": { 4 | "enabled": true, 5 | "clientKind": "git", 6 | "useIgnoreFile": true 7 | }, 8 | "files": { 9 | "ignoreUnknown": false, 10 | "ignore": [] 11 | }, 12 | "formatter": { 13 | "enabled": true, 14 | "indentStyle": "space" 15 | }, 16 | "organizeImports": { 17 | "enabled": true 18 | }, 19 | "linter": { 20 | "enabled": true, 21 | "rules": { 22 | "recommended": true 23 | } 24 | }, 25 | "javascript": { 26 | "formatter": { 27 | "quoteStyle": "double" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | branches: [main] 6 | 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | 15 | - name: Setup Bun 16 | uses: oven-sh/setup-bun@v2 17 | 18 | - name: Install dependencies 19 | run: bun install 20 | 21 | - name: Run lint 22 | run: bun run lint 23 | 24 | - name: Run typechek 25 | run: bun run typecheck 26 | 27 | - name: Run tests 28 | run: bun run test 29 | 30 | - name: Build the project 31 | run: bun run build 32 | -------------------------------------------------------------------------------- /src/utils/sendConfig.ts: -------------------------------------------------------------------------------- 1 | import type { MuxType } from "../modes/common"; 2 | import type { SendConfig } from "../types/send"; 3 | 4 | const VALID_MUX_TYPES = ["tmux", "wezterm"] as const; 5 | 6 | export function readSendConfig(): SendConfig { 7 | const muxValue = process.env.EDITPROMPT_MUX || "tmux"; 8 | 9 | if (!VALID_MUX_TYPES.includes(muxValue as MuxType)) { 10 | throw new Error( 11 | `Invalid EDITPROMPT_MUX value: ${muxValue}. Must be one of: ${VALID_MUX_TYPES.join(", ")}`, 12 | ); 13 | } 14 | 15 | const mux = muxValue as MuxType; 16 | const alwaysCopy = process.env.EDITPROMPT_ALWAYS_COPY === "1"; 17 | 18 | return { 19 | mux, 20 | alwaysCopy, 21 | }; 22 | } 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Compiled binary addons (https://nodejs.org/api/addons.html) 10 | build/Release 11 | 12 | # Dependency directories 13 | node_modules/ 14 | jspm_packages/ 15 | 16 | # TypeScript cache 17 | *.tsbuildinfo 18 | 19 | # Optional npm cache directory 20 | .npm 21 | # Optional REPL history 22 | .node_repl_history 23 | 24 | # Output of 'npm pack' 25 | *.tgz 26 | 27 | # dotenv environment variable files 28 | .env 29 | .env.* 30 | !.env.example 31 | .cache 32 | dist 33 | 34 | # Gatsby files 35 | .cache/ 36 | # vuepress v2.x temp and cache directory 37 | .temp 38 | .cache 39 | 40 | # vitepress build output 41 | **/.vitepress/dist 42 | 43 | # vitepress cache directory 44 | **/.vitepress/cache 45 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Environment setup & latest features 4 | "lib": ["ESNext"], 5 | "target": "ESNext", 6 | "module": "Preserve", 7 | "moduleDetection": "force", 8 | "jsx": "react-jsx", 9 | "allowJs": true, 10 | // Bundler mode 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "verbatimModuleSyntax": true, 14 | "noEmit": true, 15 | // Best practices 16 | "strict": true, 17 | "skipLibCheck": true, 18 | "noFallthroughCasesInSwitch": true, 19 | "noUncheckedIndexedAccess": true, 20 | "noImplicitOverride": true, 21 | // Some stricter flags (disabled by default) 22 | "noUnusedLocals": false, 23 | "noUnusedParameters": false, 24 | "noPropertyAccessFromIndexSignature": false, 25 | "resolveJsonModule": true 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/utils/envParser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Parses environment variable strings into an object. 3 | * @param envStrings - An array of strings in the format ["KEY=VALUE", "FOO=bar"]. 4 | * @returns An object of environment variable key-value pairs. 5 | */ 6 | export function parseEnvVars(envStrings?: string[]): Record { 7 | if (!envStrings || envStrings.length === 0) { 8 | return {}; 9 | } 10 | 11 | const result: Record = {}; 12 | 13 | for (const envString of envStrings) { 14 | const [key, ...valueParts] = envString.split("="); 15 | 16 | if (!key || valueParts.length === 0) { 17 | throw new Error(`Invalid environment variable format: ${envString}`); 18 | } 19 | 20 | // Handle cases where the value includes an equals sign. 21 | const value = valueParts.join("="); 22 | result[key] = value; 23 | } 24 | 25 | return result; 26 | } 27 | -------------------------------------------------------------------------------- /test/utils/contentProcessor.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "bun:test"; 2 | import { processContent } from "../../src/utils/contentProcessor"; 3 | 4 | describe("processContent", () => { 5 | test("returns plain text as-is", () => { 6 | const input = "Hello, World!"; 7 | const result = processContent(input); 8 | expect(result).toBe("Hello, World!"); 9 | }); 10 | 11 | test("removes trailing newline", () => { 12 | const input = "Hello, World!\n"; 13 | const result = processContent(input); 14 | expect(result).toBe("Hello, World!"); 15 | }); 16 | 17 | test("adds trailing space to lines ending with @", () => { 18 | const input = "foo@"; 19 | const result = processContent(input); 20 | expect(result).toBe("foo@ "); 21 | }); 22 | 23 | test("processes both @ ending and trailing newline", () => { 24 | const input = "foo@\n"; 25 | const result = processContent(input); 26 | expect(result).toBe("foo@ "); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/utils/tempFile.ts: -------------------------------------------------------------------------------- 1 | import { mkdir, writeFile } from "node:fs/promises"; 2 | import { tmpdir } from "node:os"; 3 | import { join } from "node:path"; 4 | import { TEMP_FILE_EXTENSION, TEMP_FILE_PREFIX } from "../config/constants"; 5 | 6 | function getFormattedDateTime(): string { 7 | const now = new Date(); 8 | const year = now.getFullYear(); 9 | const month = String(now.getMonth() + 1).padStart(2, "0"); 10 | const day = String(now.getDate()).padStart(2, "0"); 11 | const hours = String(now.getHours()).padStart(2, "0"); 12 | const minutes = String(now.getMinutes()).padStart(2, "0"); 13 | const seconds = String(now.getSeconds()).padStart(2, "0"); 14 | return `${year}${month}${day}${hours}${minutes}${seconds}`; 15 | } 16 | 17 | export async function createTempFile(): Promise { 18 | const tempDir = join(tmpdir(), "editprompt-prompts"); 19 | await mkdir(tempDir, { recursive: true }); 20 | const fileName = `${TEMP_FILE_PREFIX}${getFormattedDateTime()}${TEMP_FILE_EXTENSION}`; 21 | const filePath = join(tempDir, fileName); 22 | await writeFile(filePath, "", "utf-8"); 23 | return filePath; 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 eetann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /test/utils/envParser.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "bun:test"; 2 | import { parseEnvVars } from "../../src/utils/envParser"; 3 | 4 | describe("parseEnvVars", () => { 5 | test("should return empty object for undefined input", () => { 6 | expect(parseEnvVars(undefined)).toEqual({}); 7 | }); 8 | 9 | test("should return empty object for empty array", () => { 10 | expect(parseEnvVars([])).toEqual({}); 11 | }); 12 | 13 | test("should parse single environment variable", () => { 14 | expect(parseEnvVars(["FOO=bar"])).toEqual({ FOO: "bar" }); 15 | }); 16 | 17 | test("should parse multiple environment variables", () => { 18 | expect(parseEnvVars(["FOO=bar", "BAZ=qux"])).toEqual({ 19 | FOO: "bar", 20 | BAZ: "qux", 21 | }); 22 | }); 23 | 24 | test("should handle values containing equals signs", () => { 25 | expect(parseEnvVars(["URL=https://example.com?foo=bar"])).toEqual({ 26 | URL: "https://example.com?foo=bar", 27 | }); 28 | }); 29 | 30 | test("should handle empty values", () => { 31 | expect(parseEnvVars(["EMPTY="])).toEqual({ EMPTY: "" }); 32 | }); 33 | 34 | test("should throw error for invalid format without equals sign", () => { 35 | expect(() => parseEnvVars(["INVALID"])).toThrow( 36 | "Invalid environment variable format: INVALID", 37 | ); 38 | }); 39 | 40 | test("should throw error for format with only equals sign", () => { 41 | expect(() => parseEnvVars(["=value"])).toThrow( 42 | "Invalid environment variable format: =value", 43 | ); 44 | }); 45 | }); 46 | -------------------------------------------------------------------------------- /.claude/commands/commit.md: -------------------------------------------------------------------------------- 1 | --- 2 | description: "Conventional Commit" 3 | allowed-tools: Bash(git add:*), Bash(git status:*), Bash(git commit:*), Bash(git diff:*) 4 | --- 5 | 6 | # Conventional Commit Helper 7 | 8 | This command creates git commits following the Conventional Commits specification. 9 | 10 | ## Process 11 | 12 | 1. Organize current changes 13 | 2. Stage appropriate files 14 | 3. Create git commit in Conventional Commit format 15 | 16 | ### Commit Types: 17 | - **feat** - New feature addition 18 | - **fix** - Bug fix 19 | - **docs** - Documentation only changes 20 | - **style** - Changes that don't affect code behavior (whitespace, formatting, etc.) 21 | - **refactor** - Code changes that neither fix bugs nor add features 22 | - **test** - Adding tests or fixing existing tests 23 | - **chore** - Changes to build process, tools, or libraries 24 | - **build** - Changes affecting build system or external dependencies 25 | - **ci** - Changes to CI configuration files or scripts 26 | - **perf** - Performance improvements 27 | 28 | ### Format: 29 | ``` 30 | : 31 | 32 | [Details of work actually performed] 33 | ``` 34 | ※ Scope is not required for this project 35 | 36 | Please write the description **in English**. 37 | 38 | For breaking changes, add "!" after the type or include `BREAKING CHANGE:` in the footer. 39 | 40 | **Do not add Claude co-author footer to commits.** 41 | 42 | ```commit 43 | feat: add phone call cancel button 44 | 45 | Implemented cancel button in `packages/mobile/app/call.tsx`. 46 | Also adjusted margins accordingly 47 | ``` 48 | -------------------------------------------------------------------------------- /docs/write-close-send.tape: -------------------------------------------------------------------------------- 1 | Output write-close-send.gif 2 | Set Shell zsh 3 | Set FontSize 14 4 | Set Width 800 5 | Set Height 500 6 | Set Framerate 60 7 | Set Padding 10 8 | Set WindowBar Colorful 9 | Set BorderRadius 10 10 | Set FontFamily "HackGen Console NF" 11 | Set Theme "Builtin Solarized Dark" 12 | 13 | Hide 14 | Type@0 "tmux -L test kill-server" Enter 15 | Type@0 "tmux -f /dev/null -L test new-session -- zsh" Enter 16 | Type@0 "tmux set status" Enter 17 | Type@0 'tmux setw pane-border-style "fg=0"' Enter 18 | Type@0 'tmux setw pane-active-border-style "fg=0"' Enter 19 | Type@0 'tmux set -g @editprompt-cmd "node ~/ghq/github.com/eetann/editprompt/dist/index.js"' Enter 20 | Type@0 `tmux bind -N 'editprompt' q run-shell '#{@editprompt-cmd} --resume --target-pane #{pane_id} || tmux split-window -v -l 10 -c "#{pane_current_path}" "#{@editprompt-cmd} --editor nvim --target-pane #{pane_id} --always-copy"'` Enter 21 | Type@0 "clear" Enter 22 | Sleep 1 23 | Type "claude" 24 | Show 25 | 26 | Sleep 1s 27 | Enter 28 | Wait+Screen@5s /session/ # ステータスラインを待つ 29 | 30 | # キーバインドが設定されているか見る。だいたいEnter忘れ 31 | # Ctrl+B Type@0 "?" 32 | # Ctrl+D 33 | # Sleep 1s 34 | # Ctrl+D 35 | # Sleep 1s 36 | # Ctrl+D 37 | 38 | Ctrl+B Type@0 q 39 | Sleep 1s 40 | Hide 41 | Type "i" 42 | Escape 43 | Type@0 ":lua require('nvim-autopairs').disable()" Enter 44 | Sleep 50ms 45 | Show 46 | 47 | Type "i" 48 | Type "foo bar" Enter 49 | Type "```javascript" Enter 50 | Type "console.log('buz')" Enter 51 | Type "```" 52 | Sleep 1s 53 | Escape 54 | 55 | Type@300ms ":wq" Enter 56 | Sleep 3s 57 | 58 | Hide 59 | Ctrl+C 60 | Ctrl+C 61 | -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish to npm 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: read 13 | id-token: write # for OIDC 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | 19 | - name: Setup Bun 20 | uses: oven-sh/setup-bun@v2 21 | 22 | - name: Setup Node 23 | uses: actions/setup-node@v5 24 | with: 25 | node-version: "24.x" 26 | registry-url: "https://registry.npmjs.org" 27 | 28 | - name: Install dependencies 29 | run: bun install 30 | 31 | - name: Run tests 32 | run: bun test 33 | 34 | - name: Build the project 35 | run: bun run build 36 | 37 | - name: Publish to npm 38 | # Authentication will be done via OIDC, not NPM_TOKEN 39 | run: npm publish --provenance --access public 40 | # In Bun, there is no `provenance`, so use npm 41 | # https://github.com/oven-sh/bun/issues/15601 42 | 43 | changelog: 44 | needs: publish 45 | runs-on: ubuntu-latest 46 | permissions: 47 | contents: write # for creating releases 48 | steps: 49 | - uses: actions/checkout@v4 50 | with: 51 | fetch-depth: 0 52 | 53 | - name: Setup Bun 54 | uses: oven-sh/setup-bun@v2 55 | 56 | - name: Create GitHub Release 57 | run: bunx changelogithub@0.12 58 | env: 59 | # Automatically provided by GitHub Actions 60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 61 | -------------------------------------------------------------------------------- /docs/write-send-without-closing.tape: -------------------------------------------------------------------------------- 1 | Output write-send-without-closing.gif 2 | Set Shell zsh 3 | Set FontSize 14 4 | Set Width 800 5 | Set Height 500 6 | Set Framerate 60 7 | Set Padding 5 8 | Set WindowBar Colorful 9 | Set BorderRadius 10 10 | Set FontFamily "HackGen Console NF" 11 | Set Theme "Builtin Solarized Dark" 12 | 13 | Hide 14 | Type@0 "tmux -L test kill-server" Enter 15 | Type@0 "tmux -f /dev/null -L test new-session -- zsh" Enter 16 | Type@0 'tmux set status' Enter 17 | Type@0 'tmux set -g pane-border-indicators both' Enter 18 | 19 | Type@0 'tmux set -g @editprompt-cmd "node ~/ghq/github.com/eetann/editprompt/dist/index.js"' Enter 20 | Type@0 `tmux bind -N 'editprompt' q run-shell '#{@editprompt-cmd} --resume --target-pane #{pane_id} || tmux split-window -v -l 10 -c "#{pane_current_path}" "#{@editprompt-cmd} --editor nvim --target-pane #{pane_id} --always-copy"'` Enter 21 | Type@0 "clear" Enter 22 | Sleep 1 23 | Type "claude" 24 | Show 25 | 26 | Sleep 1s 27 | Enter 28 | Wait+Screen@5s /session/ # ステータスラインを待つ 29 | 30 | # キーバインドが設定されているか見る。だいたいEnter忘れ 31 | # Ctrl+B Type@0 "?" 32 | # Ctrl+D 33 | # Sleep 1s 34 | # Ctrl+D 35 | # Sleep 1s 36 | # Ctrl+D 37 | 38 | Ctrl+B Type@0 q 39 | Sleep 1s 40 | Hide 41 | Type "i" 42 | Escape 43 | Type@0 ":lua require('nvim-autopairs').disable()" Enter 44 | Type@0 ":RenderMarkdown disable" Enter 45 | Sleep 50ms 46 | Show 47 | 48 | Type "i" 49 | Type "foo bar" Enter 50 | Type "```javascript" Enter 51 | Type "console.log('buz')" Enter 52 | Type "```" 53 | Sleep 1s 54 | Escape 55 | 56 | Space Type@0 x 57 | Sleep 2s 58 | Ctrl+B Type@0 q 59 | Sleep 1s 60 | Type "i" 61 | Type "foo bar" 62 | Escape 63 | Sleep 2s 64 | 65 | Hide 66 | Ctrl+B Type@0 q 67 | Sleep 1s 68 | Ctrl+C 69 | Ctrl+C 70 | -------------------------------------------------------------------------------- /test/integration.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach, describe, expect, mock, test } from "bun:test"; 2 | 3 | mock.module("clipboardy", () => ({ 4 | default: { 5 | write: mock(), 6 | }, 7 | })); 8 | 9 | describe("Integration Tests", () => { 10 | beforeEach(() => { 11 | mock.restore(); 12 | }); 13 | 14 | describe("Module Integration", () => { 15 | test("should handle clipboard operations", async () => { 16 | const { copyToClipboard } = await import("../src/modes/common"); 17 | 18 | // Should not throw error when copying to clipboard 19 | await copyToClipboard("test content"); 20 | // If we get here without error, test passes 21 | expect(true).toBe(true); 22 | }); 23 | 24 | test("should handle editor environment variable detection", async () => { 25 | const { getEditor } = await import("../src/modules/editor"); 26 | 27 | // Test with custom environment 28 | process.env.EDITOR = "test-editor"; 29 | expect(getEditor()).toBe("test-editor"); 30 | 31 | // Test with option override 32 | expect(getEditor("override-editor")).toBe("override-editor"); 33 | }); 34 | 35 | test.skip("should handle complete workflow with mock data", async () => { 36 | // Test the main workflow without external dependencies 37 | const { getEditor } = await import("../src/modules/editor"); 38 | // sendContentToPane was removed in refactoring - skipping this test 39 | const { createTempFile } = await import("../src/utils/tempFile"); 40 | 41 | // These functions should be callable without throwing 42 | expect(() => getEditor("vim")).not.toThrow(); 43 | 44 | const tempFile = await createTempFile(); 45 | expect(typeof tempFile).toBe("string"); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import { cli } from "gunshi"; 3 | import * as pkg from "../package.json"; 4 | import { collectCommand } from "./modes/collect"; 5 | import { dumpCommand } from "./modes/dump"; 6 | import { inputCommand } from "./modes/input"; 7 | import { openCommand } from "./modes/openEditor"; 8 | import { registerCommand } from "./modes/register"; 9 | import { resumeCommand } from "./modes/resume"; 10 | 11 | const argv = process.argv.slice(2); 12 | 13 | await cli( 14 | argv, 15 | { 16 | name: "editprompt", 17 | description: 18 | "A CLI tool that lets you write prompts for CLI tools using your favorite text editor", 19 | args: {}, 20 | async run() { 21 | // Subcommand is required - show migration guide 22 | console.error("Error: Subcommand is required"); 23 | console.error(""); 24 | console.error("Migration guide from old to new syntax:"); 25 | console.error(" editprompt → editprompt open"); 26 | console.error(" editprompt --resume → editprompt resume"); 27 | console.error(' editprompt -- "text" → editprompt input "text"'); 28 | console.error(" editprompt --quote → editprompt collect"); 29 | console.error(" editprompt --capture → editprompt dump"); 30 | console.error(""); 31 | console.error( 32 | "For details: https://github.com/eetann/editprompt/?tab=readme-ov-file", 33 | ); 34 | process.exit(1); 35 | }, 36 | }, 37 | { 38 | name: "editprompt", 39 | version: pkg.version, 40 | subCommands: { 41 | open: openCommand, 42 | register: registerCommand, 43 | resume: resumeCommand, 44 | input: inputCommand, 45 | collect: collectCommand, 46 | dump: dumpCommand, 47 | }, 48 | renderHeader: null, 49 | }, 50 | ); 51 | -------------------------------------------------------------------------------- /docs/quote-capture.tape: -------------------------------------------------------------------------------- 1 | Output quote-capture.gif 2 | Set Shell zsh 3 | Set FontSize 14 4 | Set Width 800 5 | Set Height 500 6 | Set Framerate 60 7 | Set Padding 5 8 | Set WindowBar Colorful 9 | Set BorderRadius 10 10 | Set FontFamily "HackGen Console NF" 11 | Set Theme "Builtin Solarized Dark" 12 | 13 | Hide 14 | Type@0 "tmux -L test kill-server" Enter 15 | Type@0 "tmux -f /dev/null -L test new-session -- zsh" Enter 16 | Type@0 'tmux set status' Enter 17 | Type@0 'tmux set -g pane-border-indicators both' Enter 18 | 19 | Type@0 'tmux set -g @editprompt-cmd "node ~/ghq/github.com/eetann/editprompt/dist/index.js"' Enter 20 | Type@0 `tmux bind -N 'editprompt' q run-shell '#{@editprompt-cmd} --resume --target-pane #{pane_id} || tmux split-window -v -l 10 -c "#{pane_current_path}" "#{@editprompt-cmd} --editor nvim --target-pane #{pane_id} --always-copy"'` Enter 21 | Type@0 'tmux set -gw mode-keys vi' Enter 22 | # {}の記法はzshだと使えない(文字列として認識できないため) 23 | Type@0 `tmux bind -T copy-mode-vi C-e 'send-keys -X pipe "#{@editprompt-cmd} --quote --target-pane #{pane_id}" ; display "quote" '` Enter 24 | Type@0 "clear" Enter 25 | Sleep 1 26 | 27 | # 擬似的にclaudeを再現 28 | Type "cat docs/pseudo-ai-agent-output.txt" Enter 29 | # Type "tmux list-keys | grep C-e" Enter 30 | Show 31 | 32 | Ctrl+B Type@0 q 33 | 34 | Sleep 1s 35 | Hide 36 | Type "i" 37 | Escape 38 | Type@0 ":lua require('nvim-autopairs').disable()" Enter 39 | # Type@0 ":RenderMarkdown disable" Enter 40 | Sleep 50ms 41 | Show 42 | 43 | Ctrl+B Type@0 q 44 | Ctrl+B Type@0 "[" 45 | Sleep 1s 46 | 47 | Type kkkkkkkkk 48 | Sleep 1s 49 | Type Vj 50 | Sleep 1s 51 | Ctrl+E 52 | Sleep 1s 53 | 54 | Type jjjj 55 | Sleep 1s 56 | Type Vj 57 | Sleep 1s 58 | Ctrl+E 59 | Sleep 1s 60 | 61 | Ctrl+B Type@0 q 62 | 63 | Sleep 500ms 64 | Space Type@0 X 65 | Sleep 5s 66 | -------------------------------------------------------------------------------- /test/utils/sendConfig.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach, describe, expect, test } from "bun:test"; 2 | import { readSendConfig } from "../../src/utils/sendConfig"; 3 | 4 | describe("readSendConfig", () => { 5 | let originalEnv: NodeJS.ProcessEnv; 6 | 7 | beforeEach(() => { 8 | // Backup environment variables 9 | originalEnv = { ...process.env }; 10 | }); 11 | 12 | test("reads all environment variables when set", () => { 13 | process.env.EDITPROMPT_MUX = "tmux"; 14 | process.env.EDITPROMPT_ALWAYS_COPY = "1"; 15 | 16 | const config = readSendConfig(); 17 | 18 | expect(config.mux).toBe("tmux"); 19 | expect(config.alwaysCopy).toBe(true); 20 | 21 | // Restore environment variables 22 | process.env = originalEnv; 23 | }); 24 | 25 | test("throws error when EDITPROMPT_MUX has invalid value", () => { 26 | process.env.EDITPROMPT_MUX = "invalid"; 27 | process.env.EDITPROMPT_ALWAYS_COPY = "1"; 28 | 29 | expect(() => readSendConfig()).toThrow(); 30 | 31 | // Restore environment variables 32 | process.env = originalEnv; 33 | }); 34 | 35 | test("defaults to tmux when EDITPROMPT_MUX is not set", () => { 36 | process.env.EDITPROMPT_MUX = undefined; 37 | process.env.EDITPROMPT_ALWAYS_COPY = "0"; 38 | 39 | const config = readSendConfig(); 40 | 41 | expect(config.mux).toBe("tmux"); 42 | 43 | // Restore environment variables 44 | process.env = originalEnv; 45 | }); 46 | 47 | test("sets alwaysCopy to true when EDITPROMPT_ALWAYS_COPY is 1", () => { 48 | process.env.EDITPROMPT_MUX = "tmux"; 49 | process.env.EDITPROMPT_ALWAYS_COPY = "1"; 50 | 51 | const config = readSendConfig(); 52 | 53 | expect(config.alwaysCopy).toBe(true); 54 | 55 | // Restore environment variables 56 | process.env = originalEnv; 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "editprompt", 3 | "version": "1.1.1", 4 | "author": "eetann", 5 | "description": "A CLI tool that lets you write prompts for CLI tools using your favorite text editor", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/eetann/editprompt.git" 10 | }, 11 | "homepage": "https://github.com/eetann/editprompt", 12 | "bugs": { 13 | "url": "https://github.com/eetann/editprompt/issues" 14 | }, 15 | "keywords": [ 16 | "cli", 17 | "editor", 18 | "prompt", 19 | "tmux", 20 | "clipboard", 21 | "command-line", 22 | "text-editor" 23 | ], 24 | "publishConfig": { 25 | "access": "public" 26 | }, 27 | "type": "module", 28 | "files": ["dist"], 29 | "main": "./dist/index.js", 30 | "module": "./dist/index.js", 31 | "types": "./dist/index.d.ts", 32 | "exports": { 33 | ".": "./dist/index.js", 34 | "./package.json": "./package.json" 35 | }, 36 | "bin": { 37 | "editprompt": "./dist/index.js" 38 | }, 39 | "scripts": { 40 | "build": "tsdown", 41 | "dev": "tsdown --watch", 42 | "lint": "biome check", 43 | "format": "biome format --write", 44 | "typecheck": "tsgo --noEmit", 45 | "release": "bun run lint && bun run typecheck && bun run test && bun run build && bumpp", 46 | "test": "bun test", 47 | "test:watch": "bun test --watch" 48 | }, 49 | "devDependencies": { 50 | "@biomejs/biome": "1.9.4", 51 | "@types/bun": "^1.3.2", 52 | "@types/node": "^24.10.1", 53 | "@typescript/native-preview": "^7.0.0-dev.20251119.1", 54 | "bumpp": "^10.3.1", 55 | "tsdown": "latest" 56 | }, 57 | "dependencies": { 58 | "clipboardy": "^4.0.0", 59 | "conf": "^15.0.2", 60 | "gunshi": "v0.27.0-beta.3" 61 | }, 62 | "peerDependencies": { 63 | "typescript": "^5.8.3" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /test/argumentParser.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "bun:test"; 2 | import { $ } from "bun"; 3 | 4 | describe("extractRawContent (e2e with -- separator)", () => { 5 | test('test-cli.ts -- "foo --bar" should extract "foo --bar"', async () => { 6 | const result = 7 | await $`bun run test/fixtures/test-cli.ts -- -- "foo --bar"`.nothrow(); 8 | const output = result.stdout.toString().trim(); 9 | 10 | expect(output).toBe("foo --bar"); 11 | }); 12 | 13 | test("test-cli.ts -- foo --bar should extract foo --bar", async () => { 14 | const result = 15 | await $`bun run test/fixtures/test-cli.ts -- -- foo --bar`.nothrow(); 16 | const output = result.stdout.toString().trim(); 17 | 18 | expect(output).toBe("foo --bar"); 19 | }); 20 | 21 | test("bun test-cli.ts -- -- foo --bar should extract foo --bar", async () => { 22 | const result = 23 | await $`bun test/fixtures/test-cli.ts -- -- foo --bar`.nothrow(); 24 | const output = result.stdout.toString().trim(); 25 | 26 | expect(output).toBe("foo --bar"); 27 | }); 28 | 29 | test('test-cli.ts "hello world" (without -- and no options) should extract first positional', async () => { 30 | const result = 31 | await $`bun run test/fixtures/test-cli.ts -- "hello world"`.nothrow(); 32 | const output = result.stdout.toString().trim(); 33 | 34 | expect(output).toBe("hello world"); 35 | }); 36 | 37 | test('test-cli.ts "foo --bar" (without --) treats --bar as option and returns undefined', async () => { 38 | const result = 39 | await $`bun run test/fixtures/test-cli.ts -- "foo --bar"`.nothrow(); 40 | const output = result.stdout.toString().trim(); 41 | 42 | // --bar is treated as an option, so no content is extracted 43 | expect(output).toBe("undefined"); 44 | }); 45 | 46 | test("test-cli.ts -- (empty content) should return undefined", async () => { 47 | const result = await $`bun run test/fixtures/test-cli.ts -- --`.nothrow(); 48 | const output = result.stdout.toString().trim(); 49 | 50 | expect(output).toBe("undefined"); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/modes/args.ts: -------------------------------------------------------------------------------- 1 | import type { ArgSchema } from "gunshi"; 2 | import { type MuxType, SUPPORTED_MUXES, isMuxType } from "./common"; 3 | 4 | export const ARG_MUX: ArgSchema = { 5 | short: "m", 6 | description: "Multiplexer type (tmux or wezterm, default: tmux)", 7 | type: "string", 8 | }; 9 | 10 | export const ARG_TARGET_PANE_SINGLE: ArgSchema = { 11 | short: "t", 12 | description: "Target pane ID", 13 | type: "string", 14 | }; 15 | 16 | export const ARG_TARGET_PANE_MULTI: ArgSchema = { 17 | short: "t", 18 | description: "Target pane ID (can be specified multiple times)", 19 | type: "string", 20 | multiple: true, 21 | }; 22 | 23 | export const ARG_EDITOR: ArgSchema = { 24 | short: "e", 25 | description: "Editor to use (overrides $EDITOR)", 26 | type: "string", 27 | }; 28 | 29 | export const ARG_ALWAYS_COPY: ArgSchema = { 30 | description: "Always copy content to clipboard", 31 | type: "boolean", 32 | }; 33 | 34 | export const ARG_NO_QUOTE: ArgSchema = { 35 | description: "Disable quote prefix and trailing blank lines", 36 | type: "boolean", 37 | }; 38 | 39 | export const ARG_OUTPUT: ArgSchema = { 40 | description: 41 | "Output destination (buffer, stdout). Can be specified multiple times", 42 | type: "string", 43 | multiple: true, 44 | }; 45 | 46 | export function validateMux(value: unknown): MuxType { 47 | const muxValue = (value || "tmux") as string; 48 | if (!isMuxType(muxValue)) { 49 | console.error( 50 | `Error: Invalid multiplexer type '${muxValue}'. Supported values: ${SUPPORTED_MUXES.join(", ")}`, 51 | ); 52 | process.exit(1); 53 | } 54 | return muxValue; 55 | } 56 | 57 | export function validateTargetPane( 58 | value: unknown, 59 | commandName: string, 60 | ): string { 61 | if (!value || typeof value !== "string") { 62 | console.error( 63 | `Error: --target-pane is required for ${commandName} command`, 64 | ); 65 | process.exit(1); 66 | } 67 | return value; 68 | } 69 | 70 | export function normalizeTargetPanes(value: unknown): string[] { 71 | if (Array.isArray(value)) { 72 | return [...new Set(value)]; 73 | } 74 | if (typeof value === "string") { 75 | return [value]; 76 | } 77 | return []; 78 | } 79 | -------------------------------------------------------------------------------- /src/modes/dump.ts: -------------------------------------------------------------------------------- 1 | import { define } from "gunshi"; 2 | import { 3 | clearQuoteVariable, 4 | getCurrentPaneId, 5 | getQuoteVariableContent, 6 | getTargetPaneIds, 7 | isEditorPane, 8 | } from "../modules/tmux"; 9 | import * as wezterm from "../modules/wezterm"; 10 | import { readSendConfig } from "../utils/sendConfig"; 11 | 12 | export async function runDumpMode(): Promise { 13 | try { 14 | // Get config from environment variables 15 | const config = readSendConfig(); 16 | 17 | // Get current pane and check if it's an editor pane 18 | let currentPaneId: string; 19 | let isEditor: boolean; 20 | 21 | if (config.mux === "tmux") { 22 | currentPaneId = await getCurrentPaneId(); 23 | isEditor = await isEditorPane(currentPaneId); 24 | } else { 25 | currentPaneId = await wezterm.getCurrentPaneId(); 26 | isEditor = wezterm.isEditorPaneFromConf(currentPaneId); 27 | } 28 | 29 | if (!isEditor) { 30 | console.error("Error: Current pane is not an editor pane"); 31 | process.exit(1); 32 | } 33 | 34 | // Get target pane IDs from pane variables or Conf 35 | let targetPanes: string[]; 36 | if (config.mux === "tmux") { 37 | targetPanes = await getTargetPaneIds(currentPaneId); 38 | } else { 39 | targetPanes = await wezterm.getTargetPaneIds(currentPaneId); 40 | } 41 | 42 | if (targetPanes.length === 0) { 43 | console.error("Error: No target panes registered for this editor pane"); 44 | process.exit(1); 45 | } 46 | 47 | // Get and clear quote content from all target panes 48 | const quoteContents: string[] = []; 49 | for (const targetPane of targetPanes) { 50 | let content: string; 51 | if (config.mux === "tmux") { 52 | content = await getQuoteVariableContent(targetPane); 53 | await clearQuoteVariable(targetPane); 54 | } else { 55 | content = await wezterm.getQuoteText(targetPane); 56 | await wezterm.clearQuoteText(targetPane); 57 | } 58 | if (content.trim() !== "") { 59 | quoteContents.push(content); 60 | } 61 | } 62 | 63 | // Join all quote contents with newline 64 | const combinedContent = quoteContents.join("\n"); 65 | process.stdout.write(combinedContent.replace(/\n{3,}$/, "\n\n")); 66 | process.exit(0); 67 | } catch (error) { 68 | console.error( 69 | `Error: ${error instanceof Error ? error.message : "Unknown error"}`, 70 | ); 71 | process.exit(1); 72 | } 73 | } 74 | 75 | export const dumpCommand = define({ 76 | name: "dump", 77 | description: 78 | "Output and clear collected quoted text from environment variables", 79 | args: {}, 80 | async run() { 81 | await runDumpMode(); 82 | }, 83 | }); 84 | -------------------------------------------------------------------------------- /src/modules/editor.ts: -------------------------------------------------------------------------------- 1 | import { spawn } from "node:child_process"; 2 | import { readFile } from "node:fs/promises"; 3 | import { DEFAULT_EDITOR } from "../config/constants"; 4 | import type { SendConfig } from "../types/send"; 5 | import { processContent } from "../utils/contentProcessor"; 6 | import { parseEnvVars } from "../utils/envParser"; 7 | import { createTempFile } from "../utils/tempFile"; 8 | 9 | export function getEditor(editorOption?: string): string { 10 | return editorOption || process.env.EDITOR || DEFAULT_EDITOR; 11 | } 12 | 13 | export async function launchEditor( 14 | editor: string, 15 | filePath: string, 16 | envVars?: Record, 17 | sendConfig?: SendConfig, 18 | ): Promise { 19 | return new Promise((resolve, reject) => { 20 | // 環境変数の準備 21 | const configEnv: Record = {}; 22 | if (sendConfig) { 23 | configEnv.EDITPROMPT_MUX = sendConfig.mux; 24 | configEnv.EDITPROMPT_ALWAYS_COPY = sendConfig.alwaysCopy ? "1" : "0"; 25 | } 26 | 27 | const processEnv = { 28 | ...process.env, 29 | EDITPROMPT: "1", // 常に付与 30 | ...configEnv, // sendConfig由来の環境変数 31 | ...envVars, // ユーザー指定の環境変数 32 | }; 33 | 34 | const editorProcess = spawn(editor, [filePath], { 35 | stdio: "inherit", 36 | shell: true, 37 | env: processEnv, 38 | }); 39 | 40 | editorProcess.on("error", (error) => { 41 | reject(new Error(`Failed to launch editor: ${error.message}`)); 42 | }); 43 | 44 | editorProcess.on("exit", (code) => { 45 | if (code === 0) { 46 | resolve(); 47 | } else { 48 | reject(new Error(`Editor exited with code: ${code}`)); 49 | } 50 | }); 51 | }); 52 | } 53 | 54 | export async function readFileContent(filePath: string): Promise { 55 | try { 56 | const content = await readFile(filePath, "utf-8"); 57 | return processContent(content); 58 | } catch (error) { 59 | throw new Error( 60 | `Failed to read file: ${error instanceof Error ? error.message : "Unknown error"}`, 61 | ); 62 | } 63 | } 64 | 65 | export async function openEditorAndGetContent( 66 | editorOption?: string, 67 | envVars?: string[], 68 | sendConfig?: SendConfig, 69 | ): Promise { 70 | const tempFilePath = await createTempFile(); 71 | const editor = getEditor(editorOption); 72 | const parsedEnvVars = parseEnvVars(envVars); 73 | 74 | try { 75 | await launchEditor(editor, tempFilePath, parsedEnvVars, sendConfig); 76 | const content = await readFileContent(tempFilePath); 77 | 78 | return content; 79 | } catch (error) { 80 | if (error instanceof Error) { 81 | throw error; 82 | } 83 | throw new Error("An unknown error occurred"); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /docs/developer.md: -------------------------------------------------------------------------------- 1 | ## 🔧 Development 2 | 3 | ```bash 4 | # Clone the repository 5 | git clone https://github.com/eetann/editprompt.git 6 | cd editprompt 7 | 8 | # Install dependencies 9 | bun install 10 | 11 | # Build 12 | bun run build 13 | 14 | # Run tests 15 | bun test 16 | 17 | # Development mode 18 | bun run dev 19 | ``` 20 | 21 | ### 💻 Testing During Development 22 | 23 | When developing, you can test the built `dist/index.js` directly: 24 | 25 | #### Neovim Configuration 26 | 27 | ```diff 28 | - { "editprompt", "--", content }, 29 | + { "node", vim.fn.expand("~/path/to/editprompt/dist/index.js"), "--", content }, 30 | ``` 31 | 32 | #### WezTerm Configuration 33 | 34 | ```lua 35 | -- In your wezterm.lua 36 | local editprompt_cmd = "node " .. os.getenv("HOME") .. "/path/to/editprompt/dist/index.js" 37 | 38 | { 39 | key = "e", 40 | mods = "OPT", 41 | action = wezterm.action_callback(function(window, pane) 42 | local target_pane_id = tostring(pane:pane_id()) 43 | 44 | local success, stdout, stderr = wezterm.run_child_process({ 45 | "/bin/zsh", 46 | "-lc", 47 | string.format( 48 | "%s --resume --mux wezterm --target-pane %s", 49 | editprompt_cmd, 50 | target_pane_id 51 | ), 52 | }) 53 | 54 | if not success then 55 | window:perform_action( 56 | act.SplitPane({ 57 | -- ... 58 | command = { 59 | args = { 60 | "/bin/zsh", 61 | "-lc", 62 | string.format( 63 | "%s --editor nvim --always-copy --mux wezterm --target-pane %s", 64 | editprompt_cmd, 65 | target_pane_id 66 | ), 67 | }, 68 | }, 69 | }), 70 | pane 71 | ) 72 | end 73 | end), 74 | }, 75 | ``` 76 | 77 | #### tmux Configuration 78 | 79 | ```tmux 80 | # In your .tmux.conf 81 | set-option -g @editprompt-cmd "node ~/path/to/editprompt/dist/index.js" 82 | 83 | bind-key -n M-q run-shell '\ 84 | #{@editprompt-cmd} --resume --target-pane #{pane_id} || \ 85 | tmux split-window -v -l 10 -c "#{pane_current_path}" \ 86 | "#{@editprompt-cmd} --editor nvim --always-copy --target-pane #{pane_id}"' 87 | ``` 88 | 89 | This allows you to make changes, run `bun run build`, and test immediately without reinstalling globally. 90 | 91 | ## 🔍 Technical Details 92 | 93 | ### 🔄 Fallback Strategy 94 | 95 | editprompt implements a robust fallback strategy: 96 | 97 | 1. **Tmux Integration**: Direct input to tmux panes (when available) 98 | 2. **Clipboard**: Copy content to clipboard with user notification 99 | -------------------------------------------------------------------------------- /test/modules/wezterm.test.ts: -------------------------------------------------------------------------------- 1 | import { beforeEach, describe, expect, test } from "bun:test"; 2 | import { 3 | appendToQuoteText, 4 | clearQuoteText, 5 | conf, 6 | getQuoteText, 7 | } from "../../src/modules/wezterm"; 8 | 9 | beforeEach(() => { 10 | conf.clear(); 11 | }); 12 | 13 | describe("appendToQuoteText", () => { 14 | test("should create new quote_text when it doesn't exist", async () => { 15 | const paneId = "123"; 16 | const content = "> foo\n\n"; 17 | 18 | await appendToQuoteText(paneId, content); 19 | 20 | const data = conf.get(`wezterm.targetPane.pane_${paneId}`); 21 | expect(data).toEqual({ quote_text: content }); 22 | }); 23 | 24 | test("should append to existing quote_text with newline separator", async () => { 25 | const paneId = "123"; 26 | const firstContent = "> foo\n\n"; 27 | const secondContent = "> bar\n\n"; 28 | 29 | await appendToQuoteText(paneId, firstContent); 30 | await appendToQuoteText(paneId, secondContent); 31 | 32 | const data = conf.get(`wezterm.targetPane.pane_${paneId}`); 33 | expect(data).toEqual({ quote_text: `${firstContent}\n\n${secondContent}` }); 34 | }); 35 | 36 | test("should preserve existing editorPaneId when appending quote_text", async () => { 37 | const paneId = "123"; 38 | const editorPaneId = "456"; 39 | const content = "> foo\n\n"; 40 | 41 | // First, set editorPaneId 42 | conf.set(`wezterm.targetPane.pane_${paneId}`, { editorPaneId }); 43 | 44 | // Then append quote_text 45 | await appendToQuoteText(paneId, content); 46 | 47 | const data = conf.get(`wezterm.targetPane.pane_${paneId}`); 48 | expect(data).toEqual({ editorPaneId, quote_text: content }); 49 | }); 50 | }); 51 | 52 | describe("getQuoteText", () => { 53 | test("should get quote_text when it exists", async () => { 54 | const paneId = "123"; 55 | const content = "> foo\n\n"; 56 | 57 | conf.set(`wezterm.targetPane.pane_${paneId}`, { quote_text: content }); 58 | 59 | const result = await getQuoteText(paneId); 60 | expect(result).toBe(content); 61 | }); 62 | 63 | test("should return empty string when quote_text doesn't exist", async () => { 64 | const paneId = "123"; 65 | 66 | const result = await getQuoteText(paneId); 67 | expect(result).toBe(""); 68 | }); 69 | }); 70 | 71 | describe("clearQuoteText", () => { 72 | test("should delete only quote_text field", async () => { 73 | const paneId = "123"; 74 | const content = "> foo\n\n"; 75 | 76 | conf.set(`wezterm.targetPane.pane_${paneId}`, { quote_text: content }); 77 | 78 | await clearQuoteText(paneId); 79 | 80 | const data = conf.get(`wezterm.targetPane.pane_${paneId}`); 81 | expect(data).toEqual({}); 82 | }); 83 | 84 | test("should preserve editorPaneId when clearing quote_text", async () => { 85 | const paneId = "123"; 86 | const editorPaneId = "456"; 87 | const content = "> foo\n\n"; 88 | 89 | conf.set(`wezterm.targetPane.pane_${paneId}`, { 90 | editorPaneId, 91 | quote_text: content, 92 | }); 93 | 94 | await clearQuoteText(paneId); 95 | 96 | const data = conf.get(`wezterm.targetPane.pane_${paneId}`); 97 | expect(data).toEqual({ editorPaneId }); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /src/modes/register.ts: -------------------------------------------------------------------------------- 1 | import { define } from "gunshi"; 2 | import { 3 | getCurrentPaneId, 4 | getTargetPaneIds, 5 | isEditorPane, 6 | markAsEditorPane, 7 | } from "../modules/tmux"; 8 | import * as wezterm from "../modules/wezterm"; 9 | import { 10 | ARG_MUX, 11 | ARG_TARGET_PANE_MULTI, 12 | normalizeTargetPanes, 13 | validateMux, 14 | } from "./args"; 15 | import type { MuxType } from "./common"; 16 | 17 | interface RegisterModeOptions { 18 | mux: MuxType; 19 | targetPanes: string[]; 20 | editorPane?: string; 21 | } 22 | 23 | export async function runRegisterMode( 24 | options: RegisterModeOptions, 25 | ): Promise { 26 | if (options.targetPanes.length === 0) { 27 | console.error("Error: --target-pane is required for register command"); 28 | process.exit(1); 29 | } 30 | 31 | let editorPaneId: string; 32 | 33 | // Determine editor pane ID 34 | if (options.editorPane) { 35 | editorPaneId = options.editorPane; 36 | } else { 37 | // Use current pane as editor pane 38 | if (options.mux === "tmux") { 39 | editorPaneId = await getCurrentPaneId(); 40 | const isEditor = await isEditorPane(editorPaneId); 41 | if (!isEditor) { 42 | console.error( 43 | "Error: Current pane is not an editor pane. Please run this command from an editor pane or specify --editor-pane.", 44 | ); 45 | process.exit(1); 46 | } 47 | } else if (options.mux === "wezterm") { 48 | editorPaneId = await wezterm.getCurrentPaneId(); 49 | const isEditor = wezterm.isEditorPaneFromConf(editorPaneId); 50 | if (!isEditor) { 51 | console.error( 52 | "Error: Current pane is not an editor pane. Please run this command from an editor pane or specify --editor-pane.", 53 | ); 54 | process.exit(1); 55 | } 56 | } else { 57 | console.error("Error: Unsupported multiplexer"); 58 | process.exit(1); 59 | } 60 | } 61 | 62 | // Register editor pane with target panes 63 | try { 64 | // Get existing target panes 65 | let existingPanes: string[] = []; 66 | if (options.mux === "tmux") { 67 | existingPanes = await getTargetPaneIds(editorPaneId); 68 | } else if (options.mux === "wezterm") { 69 | existingPanes = await wezterm.getTargetPaneIds(editorPaneId); 70 | } 71 | 72 | // Merge with new target panes and remove duplicates 73 | const mergedTargetPanes = [ 74 | ...new Set([...existingPanes, ...options.targetPanes]), 75 | ]; 76 | 77 | // Save merged target panes 78 | if (options.mux === "tmux") { 79 | await markAsEditorPane(editorPaneId, mergedTargetPanes); 80 | } else if (options.mux === "wezterm") { 81 | await wezterm.markAsEditorPane(editorPaneId, mergedTargetPanes); 82 | } 83 | 84 | console.log( 85 | `Editor pane ${editorPaneId} registered with target panes: ${mergedTargetPanes.join(", ")}`, 86 | ); 87 | } catch (error) { 88 | console.error( 89 | `Error: ${error instanceof Error ? error.message : "Unknown error"}`, 90 | ); 91 | process.exit(1); 92 | } 93 | } 94 | 95 | export const registerCommand = define({ 96 | name: "register", 97 | description: 98 | "Register editor pane with target panes for resume mode and content delivery", 99 | args: { 100 | mux: ARG_MUX, 101 | "target-pane": ARG_TARGET_PANE_MULTI, 102 | "editor-pane": { 103 | short: "e", 104 | description: "Editor pane ID (defaults to current pane)", 105 | type: "string", 106 | }, 107 | }, 108 | async run(ctx) { 109 | const mux = validateMux(ctx.values.mux); 110 | const targetPanes = normalizeTargetPanes(ctx.values["target-pane"]); 111 | 112 | await runRegisterMode({ 113 | mux, 114 | targetPanes, 115 | editorPane: ctx.values["editor-pane"] as string | undefined, 116 | }); 117 | }, 118 | }); 119 | -------------------------------------------------------------------------------- /src/modes/common.ts: -------------------------------------------------------------------------------- 1 | import clipboardy from "clipboardy"; 2 | import { focusPane as focusTmuxPane, inputToTmuxPane } from "../modules/tmux"; 3 | import { 4 | focusPane as focusWeztermPane, 5 | inputToWeztermPane, 6 | } from "../modules/wezterm"; 7 | 8 | export type MuxType = "tmux" | "wezterm"; 9 | 10 | export function isMuxType(value: unknown): value is MuxType { 11 | return value === "tmux" || value === "wezterm"; 12 | } 13 | 14 | export const SUPPORTED_MUXES: MuxType[] = ["tmux", "wezterm"]; 15 | 16 | export interface DeliveryResult { 17 | successCount: number; 18 | totalCount: number; 19 | allSuccess: boolean; 20 | allFailed: boolean; 21 | failedPanes: string[]; 22 | } 23 | 24 | export async function copyToClipboard(content: string): Promise { 25 | await clipboardy.write(content); 26 | } 27 | 28 | async function inputContentToPane( 29 | content: string, 30 | mux: MuxType, 31 | targetPaneId: string, 32 | ): Promise { 33 | if (mux === "wezterm") { 34 | await inputToWeztermPane(targetPaneId, content); 35 | } else { 36 | await inputToTmuxPane(targetPaneId, content); 37 | } 38 | } 39 | 40 | export async function focusFirstSuccessPane( 41 | mux: MuxType, 42 | targetPanes: string[], 43 | failedPanes: string[], 44 | ): Promise { 45 | const firstSuccessPane = targetPanes.find((p) => !failedPanes.includes(p)); 46 | if (firstSuccessPane) { 47 | if (mux === "tmux") { 48 | await focusTmuxPane(firstSuccessPane); 49 | } else { 50 | await focusWeztermPane(firstSuccessPane); 51 | } 52 | } 53 | } 54 | 55 | export async function handleContentDelivery( 56 | content: string, 57 | mux: MuxType, 58 | targetPanes: string[], 59 | ): Promise { 60 | if (!content) { 61 | return { 62 | successCount: 0, 63 | totalCount: 0, 64 | allSuccess: true, 65 | allFailed: false, 66 | failedPanes: [], 67 | }; 68 | } 69 | 70 | // If no target panes, only copy to clipboard 71 | if (targetPanes.length === 0) { 72 | try { 73 | await copyToClipboard(content); 74 | console.log("Content copied to clipboard."); 75 | } catch (error) { 76 | console.log( 77 | `Failed to copy to clipboard: ${error instanceof Error ? error.message : "Unknown error"}`, 78 | ); 79 | } 80 | return { 81 | successCount: 0, 82 | totalCount: 0, 83 | allSuccess: true, 84 | allFailed: false, 85 | failedPanes: [], 86 | }; 87 | } 88 | 89 | // Send to each target pane 90 | const results: { pane: string; success: boolean }[] = []; 91 | for (const targetPane of targetPanes) { 92 | try { 93 | await inputContentToPane(content, mux, targetPane); 94 | results.push({ pane: targetPane, success: true }); 95 | } catch (error) { 96 | console.log( 97 | `Failed to send to pane ${targetPane}: ${error instanceof Error ? error.message : "Unknown error"}`, 98 | ); 99 | results.push({ pane: targetPane, success: false }); 100 | } 101 | } 102 | 103 | const successCount = results.filter((r) => r.success).length; 104 | const failedPanes = results.filter((r) => !r.success).map((r) => r.pane); 105 | const allSuccess = successCount === targetPanes.length; 106 | const allFailed = successCount === 0; 107 | 108 | // Display results 109 | if (allSuccess) { 110 | console.log("Content sent successfully to all panes!"); 111 | } else if (allFailed) { 112 | console.error("Error: All target panes failed to receive content."); 113 | console.log("Falling back to clipboard..."); 114 | await copyToClipboard(content); 115 | console.log("Content copied to clipboard."); 116 | } else { 117 | console.warn( 118 | `Warning: Content sent to ${successCount}/${targetPanes.length} panes. Failed panes: ${failedPanes.join(", ")}`, 119 | ); 120 | } 121 | 122 | return { 123 | successCount, 124 | totalCount: targetPanes.length, 125 | allSuccess, 126 | allFailed, 127 | failedPanes, 128 | }; 129 | } 130 | -------------------------------------------------------------------------------- /test/utils/quoteProcessor.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from "bun:test"; 2 | import { processQuoteText } from "../../src/utils/quoteProcessor"; 3 | 4 | describe("processQuoteText", () => { 5 | test("Pattern A: should preserve line breaks when 2nd+ lines have no leading whitespace", () => { 6 | const input = " foo\n bar\nbaz"; 7 | const expected = "> foo\n> bar\n> baz\n\n"; 8 | expect(processQuoteText(input)).toBe(expected); 9 | }); 10 | 11 | test("Pattern B: should join lines with single space when both ends are alphabetic", () => { 12 | const input = " foo\n bar\n baz"; 13 | const expected = "> foo bar baz\n\n"; 14 | expect(processQuoteText(input)).toBe(expected); 15 | }); 16 | 17 | test("Pattern B: should join lines without space when ends are not both alphabetic", () => { 18 | const input = " fooはbar\n ということが分かった"; 19 | const expected = "> fooはbarということが分かった\n\n"; 20 | expect(processQuoteText(input)).toBe(expected); 21 | }); 22 | 23 | test("Pattern B: should preserve line breaks for Markdown list items", () => { 24 | const input = 25 | " - fooって実は\n barなんだ\n - じつはhoge\n piyoには秘密がある\n - さらにbarは\n buzなんだよ"; 26 | const expected = 27 | "> - fooって実はbarなんだ\n> - じつはhoge piyoには秘密がある\n> - さらにbarはbuzなんだよ\n\n"; 28 | expect(processQuoteText(input)).toBe(expected); 29 | }); 30 | 31 | test("Pattern B: should preserve line breaks when both lines contain colons", () => { 32 | const input = " key1: value1\n key2: value2\n key3: value3"; 33 | const expected = "> key1: value1\n> key2: value2\n> key3: value3\n\n"; 34 | expect(processQuoteText(input)).toBe(expected); 35 | }); 36 | 37 | test("should add quote prefix to each line including empty lines and add two newlines at end", () => { 38 | const input = " foo\n\n bar"; 39 | const expected = "> foo\n> \n> bar\n\n"; 40 | expect(processQuoteText(input)).toBe(expected); 41 | }); 42 | 43 | test("should remove leading and trailing newlines", () => { 44 | const input = "\n foo\n bar\n"; 45 | const expected = "> foo bar\n\n"; 46 | expect(processQuoteText(input)).toBe(expected); 47 | }); 48 | 49 | test("should remove multiple leading and trailing newlines", () => { 50 | const input = "\n\n\n foo\n bar\n\n\n"; 51 | const expected = "> foo bar\n\n"; 52 | expect(processQuoteText(input)).toBe(expected); 53 | }); 54 | 55 | test("should remove only leading newlines", () => { 56 | const input = "\n\n foo\n bar"; 57 | const expected = "> foo bar\n\n"; 58 | expect(processQuoteText(input)).toBe(expected); 59 | }); 60 | 61 | test("should remove only trailing newlines", () => { 62 | const input = " foo\n bar\n\n"; 63 | const expected = "> foo bar\n\n"; 64 | expect(processQuoteText(input)).toBe(expected); 65 | }); 66 | 67 | test("should trim leading spaces on wrapped continuation lines", () => { 68 | const input = "Line one wraps\n and continues\n even more"; 69 | const expected = "> Line one wraps and continues even more\n\n"; 70 | expect(processQuoteText(input)).toBe(expected); 71 | }); 72 | 73 | test("should drop trailing spaces before wrapping", () => { 74 | const input = "- option --no- \n quote-behavior"; 75 | const expected = "> - option --no-quote-behavior\n\n"; 76 | expect(processQuoteText(input)).toBe(expected); 77 | }); 78 | 79 | test("should merge indented continuation lines even when other lines are not indented", () => { 80 | const input = `- src/modes/collect.ts: allows buffer/stdout outputs and --no- 81 | quote to skip quoting while writing to stdout. 82 | - another bullet`; 83 | const expected = 84 | "> - src/modes/collect.ts: allows buffer/stdout outputs and --no-quote to skip quoting while writing to stdout.\n> - another bullet\n\n"; 85 | expect(processQuoteText(input)).toBe(expected); 86 | }); 87 | 88 | test("should skip quote prefix and trailing newlines when withQuote is false", () => { 89 | const input = "\n foo\n bar\n\n"; 90 | const expected = "foo bar"; 91 | expect(processQuoteText(input, { withQuote: false })).toBe(expected); 92 | }); 93 | }); 94 | -------------------------------------------------------------------------------- /src/modes/resume.ts: -------------------------------------------------------------------------------- 1 | import { define } from "gunshi"; 2 | import { 3 | checkPaneExists, 4 | clearEditorPaneId, 5 | focusPane, 6 | getCurrentPaneId, 7 | getEditorPaneId, 8 | getTargetPaneIds, 9 | isEditorPane, 10 | } from "../modules/tmux"; 11 | import * as wezterm from "../modules/wezterm"; 12 | import { 13 | ARG_MUX, 14 | ARG_TARGET_PANE_SINGLE, 15 | validateMux, 16 | validateTargetPane, 17 | } from "./args"; 18 | import type { MuxType } from "./common"; 19 | 20 | export async function runResumeMode( 21 | targetPane: string, 22 | mux: MuxType, 23 | ): Promise { 24 | if (mux === "wezterm") { 25 | const currentPaneId = await wezterm.getCurrentPaneId(); 26 | const isEditor = wezterm.isEditorPaneFromConf(currentPaneId); 27 | 28 | if (isEditor) { 29 | console.log("isEditor"); 30 | const originalTargetPaneIds = 31 | await wezterm.getTargetPaneIds(currentPaneId); 32 | if (originalTargetPaneIds.length === 0) { 33 | console.log("Not found originalTargetPaneIds"); 34 | process.exit(1); 35 | } 36 | 37 | // Try to focus on the first available pane (retry logic) 38 | let focused = false; 39 | for (const paneId of originalTargetPaneIds) { 40 | const exists = await wezterm.checkPaneExists(paneId); 41 | if (exists) { 42 | await wezterm.focusPane(paneId); 43 | focused = true; 44 | break; 45 | } 46 | } 47 | 48 | if (!focused) { 49 | console.log("All target panes do not exist"); 50 | process.exit(1); 51 | } 52 | 53 | process.exit(0); 54 | } 55 | console.log("not isEditor"); 56 | 57 | // Focus from target pane to editor pane 58 | const editorPaneId = await wezterm.getEditorPaneId(targetPane); 59 | console.log(`wezterm editorPaneId: ${editorPaneId}`); 60 | 61 | if (editorPaneId === "") { 62 | console.log("Not found editorPaneId"); 63 | process.exit(1); 64 | } 65 | 66 | const exists = await wezterm.checkPaneExists(editorPaneId); 67 | if (!exists) { 68 | console.log("Not exist editorPaneId"); 69 | await wezterm.clearEditorPaneId(targetPane); 70 | process.exit(1); 71 | } 72 | 73 | try { 74 | await wezterm.focusPane(editorPaneId); 75 | process.exit(0); 76 | } catch (error) { 77 | console.log(`Can't focus editorPaneId: ${editorPaneId}\nerror: ${error}`); 78 | process.exit(1); 79 | } 80 | } 81 | 82 | // tmux logic 83 | const currentPaneId = await getCurrentPaneId(); 84 | const isEditor = await isEditorPane(currentPaneId); 85 | 86 | if (isEditor) { 87 | // Focus back to the first available target pane 88 | const originalTargetPaneIds = await getTargetPaneIds(currentPaneId); 89 | if (originalTargetPaneIds.length === 0) { 90 | process.exit(1); 91 | } 92 | 93 | // Try to focus on the first available pane (retry logic) 94 | let focused = false; 95 | for (const paneId of originalTargetPaneIds) { 96 | const exists = await checkPaneExists(paneId); 97 | if (exists) { 98 | await focusPane(paneId); 99 | focused = true; 100 | break; 101 | } 102 | } 103 | 104 | if (!focused) { 105 | // All target panes do not exist 106 | process.exit(1); 107 | } 108 | 109 | process.exit(0); 110 | } 111 | 112 | // focus to editor pane from target pane 113 | const editorPaneId = await getEditorPaneId(targetPane); 114 | 115 | if (editorPaneId === "") { 116 | process.exit(1); 117 | } 118 | 119 | const exists = await checkPaneExists(editorPaneId); 120 | if (!exists) { 121 | await clearEditorPaneId(targetPane); 122 | process.exit(1); 123 | } 124 | 125 | await focusPane(editorPaneId); 126 | process.exit(0); 127 | } 128 | 129 | export const resumeCommand = define({ 130 | name: "resume", 131 | description: "Resume existing editor pane or focus back to target pane", 132 | args: { 133 | mux: ARG_MUX, 134 | "target-pane": ARG_TARGET_PANE_SINGLE, 135 | }, 136 | async run(ctx) { 137 | const targetPane = validateTargetPane(ctx.values["target-pane"], "resume"); 138 | const mux = validateMux(ctx.values.mux); 139 | 140 | await runResumeMode(targetPane, mux); 141 | }, 142 | }); 143 | -------------------------------------------------------------------------------- /CLAUDE.md: -------------------------------------------------------------------------------- 1 | # CLAUDE.md 2 | 3 | This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 | 5 | ## Project Overview 6 | 7 | This is `editprompt`, a CLI tool that lets you write prompts for CLI tools using your favorite text editor. Originally designed for Claude Code, but now works with any CLI process. It detects running target processes and sends content to them via tmux integration or clipboard fallback. 8 | 9 | ## Development Commands 10 | 11 | ### Build and Development 12 | - `bun run build` - Build the project using tsdown 13 | - `bun run dev` - Build in watch mode for development 14 | - `bun test` - Run tests using bun 15 | - `bun test --watch` - Run tests in watch mode 16 | 17 | ### Code Quality 18 | - Format and lint using Biome (configured in `biome.jsonc`) 19 | - The project uses tab indentation and double quotes 20 | - No explicit lint/format commands in package.json - use your editor's Biome integration 21 | - **Use English for all comments and commit messages** 22 | 23 | ## Architecture 24 | 25 | ### Core Flow 26 | 1. **CLI Entry** (`src/index.ts`) - Uses gunshi for CLI parsing, orchestrates the main workflow 27 | 2. **Mode Selection** (`src/modes/`) - Routes to appropriate mode handler (openEditor, resume, sendOnly) 28 | 3. **Editor Module** (`src/modules/editor.ts`) - Handles editor launching and content extraction 29 | 4. **Process Detection** (`src/modules/process.ts`) - Finds target processes (configurable) 30 | 5. **Multiplexer Integration** (`src/modules/tmux.ts`, `src/modules/wezterm.ts`) - Handles multiplexer-specific operations 31 | 6. **Content Delivery** - Sends to multiplexer panes via `send-keys` or falls back to clipboard 32 | 33 | ### Modes 34 | - **openEditor**: Launches editor, waits for content, sends to target pane when editor closes 35 | - **resume**: Reuses existing editor panes with bidirectional focus switching 36 | - **sendOnly**: Sends content directly to target pane without opening editor (designed for in-editor execution) 37 | - **quote**: Collects text selections and stores them as quoted text (with `> ` prefix) for later retrieval 38 | - **capture**: Retrieves accumulated quoted text from quote mode and outputs to stdout, then clears storage 39 | 40 | For detailed mode implementation including constraints and solutions for different multiplexers, see [`docs/modes.md`](docs/modes.md). 41 | 42 | ### Key Design Patterns 43 | - **Multiplexer-First Strategy**: Supports both tmux and WezTerm with multiplexer-specific implementations 44 | - **Graceful Fallbacks**: Editor → Process Detection → Multiplexer → Clipboard (with user feedback) 45 | - **Process Matching**: Links system processes to multiplexer panes for accurate targeting 46 | - **Mode Separation**: Each mode has isolated logic for different use cases 47 | 48 | ### Directory Structure 49 | - `modes/`: Mode implementations (openEditor, resume, sendOnly, common) 50 | - `modules/`: Core functionality modules (editor, process, tmux, wezterm) 51 | - `utils/`: Utility functions (argumentParser, contentProcessor, envParser, sendConfig, tempFile) 52 | - `config/`: Configuration values (constants) 53 | - `types/`: TypeScript type definitions 54 | - `docs/`: Documentation files 55 | 56 | ### Module Responsibilities 57 | - `modes/openEditor.ts`: Standard editor launch and content delivery workflow 58 | - `modes/resume.ts`: Editor pane reuse with bidirectional focus switching 59 | - `modes/sendOnly.ts`: Direct content sending from within editor 60 | - `modules/editor.ts`: Editor selection ($EDITOR priority), temp file management, content extraction 61 | - `modules/process.ts`: Process discovery and content delivery mechanisms 62 | - `modules/tmux.ts`: tmux-specific operations (pane variables, focus switching) 63 | - `modules/wezterm.ts`: WezTerm-specific operations (Conf-based state management, focus switching) 64 | - `utils/tempFile.ts`: Secure temporary file creation and cleanup 65 | - `config/constants.ts`: Configuration values (default process name, file patterns, default editor) 66 | 67 | ### Dependencies 68 | - `gunshi` - CLI framework 69 | - `clipboardy` - Clipboard operations 70 | - `conf` - Persistent configuration storage (used for WezTerm state management) 71 | - Native Node.js modules for multiplexer integration and file operations 72 | 73 | ## Testing 74 | 75 | Tests are located in `test/` directory with structure mirroring `src/`. Uses bun test runner with integration tests covering the full workflow. 76 | -------------------------------------------------------------------------------- /src/modes/collect.ts: -------------------------------------------------------------------------------- 1 | import { define } from "gunshi"; 2 | import { appendToQuoteVariable } from "../modules/tmux"; 3 | import { appendToQuoteText } from "../modules/wezterm"; 4 | import { extractRawContent } from "../utils/argumentParser"; 5 | import { processQuoteText } from "../utils/quoteProcessor"; 6 | import { 7 | ARG_MUX, 8 | ARG_NO_QUOTE, 9 | ARG_OUTPUT, 10 | ARG_TARGET_PANE_SINGLE, 11 | validateMux, 12 | validateTargetPane, 13 | } from "./args"; 14 | import type { MuxType } from "./common"; 15 | 16 | type CollectOutput = "buffer" | "stdout"; 17 | 18 | const SUPPORTED_OUTPUTS: CollectOutput[] = ["buffer", "stdout"]; 19 | 20 | async function readStdin(): Promise { 21 | return new Promise((resolve, reject) => { 22 | const chunks: Buffer[] = []; 23 | process.stdin.on("data", (chunk) => { 24 | chunks.push(chunk); 25 | }); 26 | process.stdin.on("end", () => { 27 | resolve(Buffer.concat(chunks).toString("utf8")); 28 | }); 29 | process.stdin.on("error", (error) => { 30 | reject(error); 31 | }); 32 | }); 33 | } 34 | 35 | function normalizeCollectOutputs(value: unknown): CollectOutput[] { 36 | let outputs: string[] = []; 37 | 38 | if (Array.isArray(value)) { 39 | outputs = value.map((v) => String(v)); 40 | } else if (typeof value === "string") { 41 | outputs = [value]; 42 | } 43 | 44 | if (outputs.length === 0) { 45 | return ["buffer"]; 46 | } 47 | 48 | const uniqueOutputs = [...new Set(outputs)]; 49 | const invalid = uniqueOutputs.filter( 50 | (v) => !SUPPORTED_OUTPUTS.includes(v as CollectOutput), 51 | ); 52 | 53 | if (invalid.length > 0) { 54 | console.error( 55 | `Error: Invalid output(s) '${invalid.join(", ")}'. Supported values: ${SUPPORTED_OUTPUTS.join(", ")}`, 56 | ); 57 | process.exit(1); 58 | } 59 | 60 | return uniqueOutputs as CollectOutput[]; 61 | } 62 | 63 | export async function runCollectMode( 64 | mux: MuxType, 65 | targetPaneId: string, 66 | rawContent?: string, 67 | outputs: CollectOutput[] = ["buffer"], 68 | withQuote = true, 69 | ): Promise { 70 | try { 71 | let selection: string; 72 | 73 | if (rawContent !== undefined) { 74 | // Use positional argument (wezterm) 75 | selection = rawContent; 76 | } else { 77 | // Read from stdin (tmux) 78 | selection = await readStdin(); 79 | } 80 | 81 | // Process text 82 | const processedText = processQuoteText(selection, { withQuote }); 83 | 84 | for (const output of outputs) { 85 | if (output === "buffer") { 86 | if (mux === "tmux") { 87 | await appendToQuoteVariable(targetPaneId, processedText); 88 | } else if (mux === "wezterm") { 89 | await appendToQuoteText(targetPaneId, processedText); 90 | } 91 | } else if (output === "stdout") { 92 | process.stdout.write(processedText); 93 | } 94 | } 95 | } catch (error) { 96 | console.error( 97 | `Error: ${error instanceof Error ? error.message : "Unknown error"}`, 98 | ); 99 | process.exit(1); 100 | } 101 | } 102 | 103 | export const collectCommand = define({ 104 | name: "collect", 105 | description: "Collect and accumulate quoted text to pane variable", 106 | args: { 107 | mux: ARG_MUX, 108 | "target-pane": ARG_TARGET_PANE_SINGLE, 109 | output: ARG_OUTPUT, 110 | "no-quote": ARG_NO_QUOTE, 111 | }, 112 | async run(ctx) { 113 | const targetPane = validateTargetPane(ctx.values["target-pane"], "collect"); 114 | const mux = validateMux(ctx.values.mux); 115 | const outputs = normalizeCollectOutputs(ctx.values.output); 116 | const withQuote = !ctx.values["no-quote"]; 117 | 118 | // For wezterm, content must be provided as argument 119 | // For tmux, content is read from stdin 120 | let rawContent: string | undefined; 121 | if (mux === "wezterm") { 122 | rawContent = extractRawContent(ctx.rest, ctx.positionals); 123 | if (rawContent === undefined) { 124 | console.error( 125 | 'Error: Text content is required for collect mode with wezterm. Use: editprompt collect --mux wezterm --target-pane -- ""', 126 | ); 127 | process.exit(1); 128 | } 129 | } 130 | 131 | await runCollectMode(mux, targetPane, rawContent, outputs, withQuote); 132 | }, 133 | }); 134 | -------------------------------------------------------------------------------- /docs/migration-guide-v1.md: -------------------------------------------------------------------------------- 1 | # Migration Guide: v0.8.1 → v1.0.0 2 | 3 | This guide helps you migrate from editprompt v0.8.1 (and earlier) to v1.0.0, which introduces a breaking change to the CLI interface. 4 | 5 | ## What Changed? 6 | 7 | **v1.0.0 migrates from option-based modes to explicit subcommands** for better CLI consistency and usability. This is a **breaking change** - old command syntax will no longer work. 8 | 9 | ### Why This Change? 10 | 11 | The old syntax mixed different patterns (no options, `--resume`, `-- "content"`), making the CLI confusing and inconsistent. The new subcommand-based approach provides: 12 | 13 | - **Consistent interface**: All modes use the same `editprompt ` pattern 14 | - **Better discoverability**: `editprompt --help` clearly shows available subcommands 15 | - **Easier maintenance**: Each subcommand has its own isolated definition and help 16 | - **Future extensibility**: Adding new modes is simpler and clearer 17 | 18 | ## Command Migration Table 19 | 20 | | Old Command (v0.8.1) | New Command (v1.0.0) | Description | 21 | |----------------------|----------------------|-------------| 22 | | `editprompt` | `editprompt open` | Open editor and send content | 23 | | `editprompt --resume` | `editprompt resume` | Resume existing editor pane | 24 | | `editprompt -- "text"` | `editprompt input -- "text"` | Send content directly | 25 | | `editprompt --auto-send -- "text"` | `editprompt input --auto-send -- "text"` | Send content with auto-submit | 26 | | `editprompt --quote` | `editprompt collect` | Collect quoted text | 27 | | `editprompt --capture` | `editprompt dump` | Output and clear collected quotes | 28 | 29 | ## Detailed Migration Examples 30 | 31 | ### Basic Editor Launch 32 | 33 | **Old:** 34 | ```bash 35 | editprompt 36 | editprompt --editor nvim 37 | editprompt --always-copy 38 | ``` 39 | 40 | **New:** 41 | ```bash 42 | editprompt open 43 | editprompt open --editor nvim 44 | editprompt open --always-copy 45 | ``` 46 | 47 | ### Resume Mode 48 | 49 | **Old:** 50 | ```bash 51 | editprompt --resume --target-pane %123 52 | ``` 53 | 54 | **New:** 55 | ```bash 56 | editprompt resume --target-pane %123 57 | ``` 58 | 59 | ### Send Without Closing Editor 60 | 61 | **Old:** 62 | ```bash 63 | editprompt -- "your content" 64 | editprompt --auto-send -- "your content" 65 | editprompt --auto-send --send-key "C-m" -- "your content" 66 | ``` 67 | 68 | **New:** 69 | ```bash 70 | editprompt input -- "your content" 71 | editprompt input --auto-send -- "your content" 72 | editprompt input --auto-send --send-key "C-m" -- "your content" 73 | ``` 74 | 75 | 76 | ### Quote Collection 77 | 78 | **Old:** 79 | ```bash 80 | editprompt --quote --target-pane %123 81 | ``` 82 | 83 | **New:** 84 | ```bash 85 | editprompt collect --target-pane %123 86 | ``` 87 | 88 | ### Quote Capture 89 | 90 | **Old:** 91 | ```bash 92 | editprompt --capture 93 | ``` 94 | 95 | **New:** 96 | ```bash 97 | editprompt dump 98 | ``` 99 | 100 | ## Getting Help 101 | 102 | ### Subcommand List 103 | 104 | Run `editprompt --help` to see all available subcommands: 105 | 106 | ```bash 107 | $ editprompt --help 108 | ``` 109 | 110 | ### Subcommand-Specific Help 111 | 112 | Run `editprompt --help` for detailed help on each subcommand: 113 | 114 | ```bash 115 | $ editprompt open --help 116 | $ editprompt resume --help 117 | $ editprompt input --help 118 | $ editprompt collect --help 119 | $ editprompt dump --help 120 | ``` 121 | 122 | ## Troubleshooting 123 | 124 | ### Error: "Subcommand is required" 125 | 126 | If you see this error, you're using the old syntax. Follow the migration table above to update your command. 127 | 128 | ```bash 129 | # ❌ Old (will fail) 130 | $ editprompt 131 | 132 | # ✅ New 133 | $ editprompt open 134 | ``` 135 | 136 | ### Updated Bindings Not Working 137 | 138 | After updating your configuration files: 139 | 140 | 1. **tmux**: Reload configuration with `tmux source-file ~/.tmux.conf` or restart tmux 141 | 2. **WezTerm**: Reload configuration with `Ctrl+Shift+R` or restart WezTerm 142 | 3. **Neovim**: restart Neovim 143 | 144 | ## Summary 145 | 146 | 1. **Update all commands** from option-based to subcommand-based syntax 147 | 2. **Update configuration files** (`.tmux.conf`, `wezterm.lua`, editor configs) 148 | 3. **Reload configurations** in your multiplexer and editor 149 | 4. **Use `--help`** to explore new subcommand interface 150 | -------------------------------------------------------------------------------- /src/modes/openEditor.ts: -------------------------------------------------------------------------------- 1 | import { define } from "gunshi"; 2 | import { openEditorAndGetContent } from "../modules/editor"; 3 | import { 4 | clearEditorPaneId, 5 | getCurrentPaneId, 6 | markAsEditorPane, 7 | } from "../modules/tmux"; 8 | import * as wezterm from "../modules/wezterm"; 9 | import type { SendConfig } from "../types/send"; 10 | import { 11 | ARG_ALWAYS_COPY, 12 | ARG_EDITOR, 13 | ARG_MUX, 14 | ARG_TARGET_PANE_MULTI, 15 | normalizeTargetPanes, 16 | validateMux, 17 | } from "./args"; 18 | import { 19 | type MuxType, 20 | copyToClipboard, 21 | focusFirstSuccessPane, 22 | handleContentDelivery, 23 | } from "./common"; 24 | 25 | interface OpenEditorModeOptions { 26 | mux: MuxType; 27 | targetPanes: string[]; 28 | alwaysCopy: boolean; 29 | editor?: string; 30 | env?: string[]; 31 | } 32 | 33 | export async function runOpenEditorMode( 34 | options: OpenEditorModeOptions, 35 | ): Promise { 36 | if (options.targetPanes.length > 0 && options.mux === "tmux") { 37 | try { 38 | const currentPaneId = await getCurrentPaneId(); 39 | await markAsEditorPane(currentPaneId, options.targetPanes); 40 | } catch { 41 | // 42 | } 43 | } else if (options.targetPanes.length > 0 && options.mux === "wezterm") { 44 | try { 45 | const currentPaneId = await wezterm.getCurrentPaneId(); 46 | await wezterm.markAsEditorPane(currentPaneId, options.targetPanes); 47 | } catch { 48 | // 49 | } 50 | } 51 | 52 | try { 53 | const sendConfig: SendConfig = { 54 | mux: options.mux, 55 | alwaysCopy: options.alwaysCopy, 56 | }; 57 | 58 | console.log("Opening editor..."); 59 | 60 | const content = await openEditorAndGetContent( 61 | options.editor, 62 | options.env, 63 | sendConfig, 64 | ); 65 | 66 | if (!content) { 67 | console.log("No content entered. Exiting."); 68 | return; 69 | } 70 | 71 | try { 72 | const result = await handleContentDelivery( 73 | content, 74 | options.mux, 75 | options.targetPanes, 76 | ); 77 | 78 | // Output content for reference 79 | console.log("---"); 80 | console.log(content); 81 | 82 | // Copy to clipboard if alwaysCopy is enabled 83 | if (options.alwaysCopy && !result.allFailed) { 84 | await copyToClipboard(content); 85 | console.log("Also copied to clipboard."); 86 | } 87 | 88 | // Focus on the first successful pane 89 | if (options.targetPanes.length > 0 && result.successCount > 0) { 90 | await focusFirstSuccessPane( 91 | options.mux, 92 | options.targetPanes, 93 | result.failedPanes, 94 | ); 95 | } 96 | 97 | // Exit with code 1 if not all panes succeeded (requirement 6) 98 | if (!result.allSuccess) { 99 | process.exit(1); 100 | } 101 | } catch (error) { 102 | console.error( 103 | `Error: ${error instanceof Error ? error.message : "Unknown error"}`, 104 | ); 105 | process.exit(1); 106 | } 107 | } finally { 108 | if (options.targetPanes.length > 0 && options.mux === "tmux") { 109 | try { 110 | for (const targetPane of options.targetPanes) { 111 | await clearEditorPaneId(targetPane); 112 | } 113 | } catch { 114 | // 115 | } 116 | } else if (options.targetPanes.length > 0 && options.mux === "wezterm") { 117 | try { 118 | for (const targetPane of options.targetPanes) { 119 | await wezterm.clearEditorPaneId(targetPane); 120 | } 121 | } catch { 122 | // 123 | } 124 | } 125 | } 126 | } 127 | 128 | export const openCommand = define({ 129 | name: "open", 130 | description: "Open editor and send content to target pane", 131 | args: { 132 | mux: ARG_MUX, 133 | "target-pane": ARG_TARGET_PANE_MULTI, 134 | editor: ARG_EDITOR, 135 | "always-copy": ARG_ALWAYS_COPY, 136 | env: { 137 | short: "E", 138 | description: "Environment variables to set (e.g., KEY=VALUE)", 139 | type: "string", 140 | multiple: true, 141 | }, 142 | }, 143 | async run(ctx) { 144 | const mux = validateMux(ctx.values.mux); 145 | const targetPanes = normalizeTargetPanes(ctx.values["target-pane"]); 146 | 147 | await runOpenEditorMode({ 148 | mux, 149 | targetPanes, 150 | alwaysCopy: Boolean(ctx.values["always-copy"]), 151 | editor: ctx.values.editor as string | undefined, 152 | env: ctx.values.env as string[] | undefined, 153 | }); 154 | }, 155 | }); 156 | -------------------------------------------------------------------------------- /src/modules/tmux.ts: -------------------------------------------------------------------------------- 1 | import { exec } from "node:child_process"; 2 | import { promisify } from "node:util"; 3 | 4 | const execAsync = promisify(exec); 5 | 6 | export async function getCurrentPaneId(): Promise { 7 | const { stdout } = await execAsync('tmux display-message -p "#{pane_id}"'); 8 | return stdout.trim(); 9 | } 10 | 11 | export async function saveEditorPaneId( 12 | targetPaneId: string, 13 | editorPaneId: string, 14 | ): Promise { 15 | await execAsync( 16 | `tmux set-option -pt '${targetPaneId}' @editprompt_editor_pane '${editorPaneId}'`, 17 | ); 18 | } 19 | 20 | export async function clearEditorPaneId(targetPaneId: string): Promise { 21 | await execAsync( 22 | `tmux set-option -pt '${targetPaneId}' @editprompt_editor_pane ""`, 23 | ); 24 | } 25 | 26 | export async function getEditorPaneId(targetPaneId: string): Promise { 27 | try { 28 | const { stdout } = await execAsync( 29 | `tmux show -pt '${targetPaneId}' -v @editprompt_editor_pane`, 30 | ); 31 | return stdout.trim(); 32 | } catch { 33 | return ""; 34 | } 35 | } 36 | 37 | export async function checkPaneExists(paneId: string): Promise { 38 | try { 39 | const { stdout } = await execAsync('tmux list-panes -a -F "#{pane_id}"'); 40 | const paneIds = stdout.split("\n").map((id) => id.trim()); 41 | return paneIds.includes(paneId); 42 | } catch { 43 | return false; 44 | } 45 | } 46 | 47 | export async function focusPane(paneId: string): Promise { 48 | await execAsync(`tmux select-pane -t '${paneId}'`); 49 | } 50 | 51 | export async function markAsEditorPane( 52 | editorPaneId: string, 53 | targetPaneIds: string[], 54 | ): Promise { 55 | await execAsync( 56 | `tmux set-option -pt '${editorPaneId}' @editprompt_is_editor 1`, 57 | ); 58 | const uniqueTargetPaneIds = [...new Set(targetPaneIds)]; 59 | const targetPanesValue = uniqueTargetPaneIds.join(","); 60 | await execAsync( 61 | `tmux set-option -pt '${editorPaneId}' @editprompt_target_panes '${targetPanesValue}'`, 62 | ); 63 | // Save editor pane ID to each target pane 64 | for (const targetPaneId of uniqueTargetPaneIds) { 65 | await saveEditorPaneId(targetPaneId, editorPaneId); 66 | } 67 | } 68 | 69 | export async function getTargetPaneIds( 70 | editorPaneId: string, 71 | ): Promise { 72 | try { 73 | const { stdout } = await execAsync( 74 | `tmux show -pt '${editorPaneId}' -v @editprompt_target_panes`, 75 | ); 76 | const value = stdout.trim(); 77 | if (value === "") { 78 | return []; 79 | } 80 | return value.split(",").map((id) => id.trim()); 81 | } catch { 82 | return []; 83 | } 84 | } 85 | 86 | export async function isEditorPane(paneId: string): Promise { 87 | try { 88 | const { stdout } = await execAsync( 89 | `tmux show -pt '${paneId}' -v @editprompt_is_editor`, 90 | ); 91 | return stdout.trim() === "1"; 92 | } catch { 93 | return false; 94 | } 95 | } 96 | 97 | export async function getQuoteVariableContent(paneId: string): Promise { 98 | try { 99 | const { stdout } = await execAsync( 100 | `tmux show -pt '${paneId}' -v @editprompt_quote`, 101 | ); 102 | return stdout; 103 | } catch { 104 | return ""; 105 | } 106 | } 107 | 108 | export async function appendToQuoteVariable( 109 | paneId: string, 110 | content: string, 111 | ): Promise { 112 | let newContent = ""; 113 | const existingContent = await getQuoteVariableContent(paneId); 114 | if (existingContent.trim() !== "") { 115 | newContent = `${existingContent}\n${content}`; 116 | } else { 117 | newContent = content; 118 | } 119 | await execAsync( 120 | `tmux set-option -pt '${paneId}' @editprompt_quote '${newContent.replace(/'/g, "'\\''")}' `, 121 | ); 122 | } 123 | 124 | export async function clearQuoteVariable(targetPaneId: string): Promise { 125 | await execAsync(`tmux set-option -pt '${targetPaneId}' @editprompt_quote ""`); 126 | } 127 | 128 | export async function sendKeyToTmuxPane( 129 | paneId: string, 130 | key: string, 131 | ): Promise { 132 | // Sleep so as not to be treated as a newline (e.g., codex) 133 | await new Promise((resolve) => setTimeout(resolve, 100)); 134 | await execAsync(`tmux send-keys -t '${paneId}' '${key}'`); 135 | } 136 | 137 | export async function inputToTmuxPane( 138 | paneId: string, 139 | content: string, 140 | ): Promise { 141 | // Exit copy mode if the pane is in copy mode 142 | await execAsync( 143 | `tmux if-shell -t '${paneId}' '[ "#{pane_in_mode}" = "1" ]' "copy-mode -q -t '${paneId}'"`, 144 | ); 145 | 146 | // Send content using send-keys command (no focus change) 147 | await execAsync( 148 | `tmux send-keys -t '${paneId}' -- '${content.replace(/'/g, "'\\''")}'`, 149 | ); 150 | console.log(`Content sent to tmux pane: ${paneId}`); 151 | } 152 | -------------------------------------------------------------------------------- /src/modes/input.ts: -------------------------------------------------------------------------------- 1 | import { define } from "gunshi"; 2 | import { 3 | getCurrentPaneId, 4 | getTargetPaneIds, 5 | inputToTmuxPane, 6 | isEditorPane, 7 | sendKeyToTmuxPane, 8 | } from "../modules/tmux"; 9 | import * as wezterm from "../modules/wezterm"; 10 | import { extractRawContent } from "../utils/argumentParser"; 11 | import { processContent } from "../utils/contentProcessor"; 12 | import { readSendConfig } from "../utils/sendConfig"; 13 | import { 14 | copyToClipboard, 15 | focusFirstSuccessPane, 16 | handleContentDelivery, 17 | } from "./common"; 18 | 19 | export async function runInputMode( 20 | rawContent: string, 21 | autoSend?: boolean, 22 | sendKey?: string, 23 | ): Promise { 24 | const content = processContent(rawContent); 25 | 26 | if (!content) { 27 | console.log("No content to send. Exiting."); 28 | return; 29 | } 30 | 31 | const config = readSendConfig(); 32 | 33 | // Get current pane and check if it's an editor pane 34 | let currentPaneId: string; 35 | let isEditor: boolean; 36 | 37 | if (config.mux === "tmux") { 38 | currentPaneId = await getCurrentPaneId(); 39 | isEditor = await isEditorPane(currentPaneId); 40 | } else { 41 | currentPaneId = await wezterm.getCurrentPaneId(); 42 | isEditor = wezterm.isEditorPaneFromConf(currentPaneId); 43 | } 44 | 45 | if (!isEditor) { 46 | console.error("Error: Current pane is not an editor pane"); 47 | process.exit(1); 48 | } 49 | 50 | // Get target pane IDs from pane variables or Conf 51 | let targetPanes: string[]; 52 | if (config.mux === "tmux") { 53 | targetPanes = await getTargetPaneIds(currentPaneId); 54 | } else { 55 | targetPanes = await wezterm.getTargetPaneIds(currentPaneId); 56 | } 57 | 58 | if (targetPanes.length === 0) { 59 | console.error("Error: No target panes registered for this editor pane"); 60 | process.exit(1); 61 | } 62 | 63 | // Auto-send mode 64 | if (autoSend) { 65 | const key = sendKey || (config.mux === "wezterm" ? "\\r" : "C-m"); 66 | 67 | let successCount = 0; 68 | for (const targetPane of targetPanes) { 69 | try { 70 | if (config.mux === "wezterm") { 71 | await wezterm.inputToWeztermPane(targetPane, content); 72 | await wezterm.sendKeyToWeztermPane(targetPane, key); 73 | } else { 74 | await inputToTmuxPane(targetPane, content); 75 | await sendKeyToTmuxPane(targetPane, key); 76 | } 77 | successCount++; 78 | } catch (error) { 79 | console.error( 80 | `Failed to send to pane ${targetPane}: ${error instanceof Error ? error.message : "Unknown error"}`, 81 | ); 82 | } 83 | } 84 | if (config.alwaysCopy) { 85 | await copyToClipboard(content); 86 | console.log("Also copied to clipboard."); 87 | } 88 | 89 | if (successCount > 0) { 90 | console.log("Content sent and submitted successfully!"); 91 | } else { 92 | console.error("Error: All target panes failed to receive content"); 93 | process.exit(1); 94 | } 95 | return; 96 | } 97 | 98 | // Normal mode (focus on first successful pane) 99 | try { 100 | const result = await handleContentDelivery( 101 | content, 102 | config.mux, 103 | targetPanes, 104 | ); 105 | 106 | // Copy to clipboard if alwaysCopy is enabled 107 | if (config.alwaysCopy && !result.allFailed) { 108 | await copyToClipboard(content); 109 | console.log("Also copied to clipboard."); 110 | } 111 | 112 | // Focus on the first successful pane 113 | if (result.successCount > 0) { 114 | await focusFirstSuccessPane(config.mux, targetPanes, result.failedPanes); 115 | } 116 | 117 | // Exit with code 1 if all panes failed (requirement 3) 118 | if (result.allFailed) { 119 | process.exit(1); 120 | } 121 | } catch (error) { 122 | console.error( 123 | `Error: ${error instanceof Error ? error.message : "Unknown error"}`, 124 | ); 125 | process.exit(1); 126 | } 127 | } 128 | 129 | export const inputCommand = define({ 130 | name: "input", 131 | description: "Send content directly to target pane without opening editor", 132 | args: { 133 | "auto-send": { 134 | description: "Automatically send Enter key after content", 135 | type: "boolean", 136 | }, 137 | "send-key": { 138 | description: "Key to send after content (requires --auto-send)", 139 | type: "string", 140 | }, 141 | }, 142 | async run(ctx) { 143 | // Get content from positional arguments or after -- 144 | const rawContent = extractRawContent(ctx.rest, ctx.positionals); 145 | 146 | if (rawContent === undefined) { 147 | console.error("Error: Content is required for input command"); 148 | console.error('Usage: editprompt input "your content"'); 149 | console.error(' or: editprompt input -- "your content"'); 150 | process.exit(1); 151 | } 152 | 153 | // Validate --send-key requires --auto-send 154 | if (ctx.values["send-key"] && !ctx.values["auto-send"]) { 155 | console.error("Error: --send-key requires --auto-send to be enabled"); 156 | process.exit(1); 157 | } 158 | 159 | await runInputMode( 160 | rawContent, 161 | Boolean(ctx.values["auto-send"]), 162 | ctx.values["send-key"] as string | undefined, 163 | ); 164 | }, 165 | }); 166 | -------------------------------------------------------------------------------- /src/modules/wezterm.ts: -------------------------------------------------------------------------------- 1 | import { exec } from "node:child_process"; 2 | import { promisify } from "node:util"; 3 | import Conf from "conf"; 4 | 5 | const execAsync = promisify(exec); 6 | 7 | const projectName = 8 | process.env.NODE_ENV === "test" ? "editprompt-test" : "editprompt"; 9 | export const conf = new Conf({ projectName }); 10 | 11 | interface WeztermPane { 12 | pane_id: string; 13 | is_active: boolean; 14 | } 15 | 16 | export async function getCurrentPaneId(): Promise { 17 | try { 18 | const { stdout } = await execAsync("wezterm cli list --format json"); 19 | const panes = JSON.parse(stdout) as WeztermPane[]; 20 | const activePane = panes.find((pane) => pane.is_active === true); 21 | return String(activePane?.pane_id); 22 | } catch (error) { 23 | console.log(error); 24 | return ""; 25 | } 26 | } 27 | 28 | export async function checkPaneExists(paneId: string): Promise { 29 | try { 30 | const { stdout } = await execAsync("wezterm cli list --format json"); 31 | console.log(stdout); 32 | const panes = JSON.parse(stdout) as WeztermPane[]; 33 | return panes.some((pane) => String(pane.pane_id) === paneId); 34 | } catch (error) { 35 | console.log(error); 36 | return false; 37 | } 38 | } 39 | 40 | export async function saveEditorPaneId( 41 | targetPaneId: string, 42 | editorPaneId: string, 43 | ): Promise { 44 | console.log(`wezterm.targetPane.pane_${targetPaneId}`); 45 | try { 46 | conf.set(`wezterm.targetPane.pane_${targetPaneId}`, { 47 | editorPaneId: editorPaneId, 48 | }); 49 | } catch (error) { 50 | console.log(error); 51 | } 52 | } 53 | 54 | export async function getEditorPaneId(targetPaneId: string): Promise { 55 | try { 56 | const data = conf.get(`wezterm.targetPane.pane_${targetPaneId}`); 57 | if (typeof data === "object" && data !== null && "editorPaneId" in data) { 58 | return String(data.editorPaneId); 59 | } 60 | return ""; 61 | } catch (error) { 62 | console.log(error); 63 | return ""; 64 | } 65 | } 66 | 67 | export async function clearEditorPaneId(targetPaneId: string): Promise { 68 | try { 69 | const editorPaneId = await getEditorPaneId(targetPaneId); 70 | conf.delete(`wezterm.targetPane.pane_${targetPaneId}`); 71 | if (editorPaneId) { 72 | conf.delete(`wezterm.editorPane.pane_${editorPaneId}`); 73 | } 74 | } catch (error) { 75 | console.log(error); 76 | } 77 | } 78 | 79 | export async function focusPane(paneId: string): Promise { 80 | await execAsync(`wezterm cli activate-pane --pane-id '${paneId}'`); 81 | } 82 | 83 | export function isEditorPaneFromEnv(): boolean { 84 | return process.env.EDITPROMPT_IS_EDITOR === "1"; 85 | } 86 | 87 | export function getTargetPaneIdFromEnv(): string | undefined { 88 | return process.env.EDITPROMPT_TARGET_PANE; 89 | } 90 | 91 | export async function markAsEditorPane( 92 | editorPaneId: string, 93 | targetPaneIds: string[], 94 | ): Promise { 95 | try { 96 | const uniqueTargetPaneIds = [...new Set(targetPaneIds)]; 97 | conf.set(`wezterm.editorPane.pane_${editorPaneId}`, { 98 | targetPaneIds: uniqueTargetPaneIds, 99 | }); 100 | // Save editor pane ID to each target pane 101 | for (const targetPaneId of uniqueTargetPaneIds) { 102 | await saveEditorPaneId(targetPaneId, editorPaneId); 103 | } 104 | } catch (error) { 105 | console.log(error); 106 | } 107 | } 108 | 109 | export async function getTargetPaneIds( 110 | editorPaneId: string, 111 | ): Promise { 112 | try { 113 | const data = conf.get(`wezterm.editorPane.pane_${editorPaneId}`); 114 | if (typeof data === "object" && data !== null && "targetPaneIds" in data) { 115 | const targetPaneIds = data.targetPaneIds; 116 | if (Array.isArray(targetPaneIds)) { 117 | return targetPaneIds.map((id) => String(id)); 118 | } 119 | } 120 | return []; 121 | } catch (error) { 122 | console.log(error); 123 | return []; 124 | } 125 | } 126 | 127 | export function isEditorPaneFromConf(paneId: string): boolean { 128 | try { 129 | return conf.has(`wezterm.editorPane.pane_${paneId}`); 130 | } catch (error) { 131 | console.log(error); 132 | return false; 133 | } 134 | } 135 | 136 | export async function appendToQuoteText( 137 | paneId: string, 138 | content: string, 139 | ): Promise { 140 | try { 141 | const data = conf.get(`wezterm.targetPane.pane_${paneId}`); 142 | let newData: Record; 143 | 144 | if (typeof data === "object" && data !== null) { 145 | // Existing data exists, preserve it and add/update quote_text 146 | const existingQuoteText = 147 | "quote_text" in data ? String(data.quote_text) : ""; 148 | const newQuoteText = 149 | existingQuoteText.trim() !== "" 150 | ? `${existingQuoteText}\n\n${content}` 151 | : content; 152 | 153 | newData = { 154 | ...data, 155 | quote_text: newQuoteText, 156 | }; 157 | } else { 158 | // No existing data, create new 159 | newData = { quote_text: content }; 160 | } 161 | 162 | conf.set(`wezterm.targetPane.pane_${paneId}`, newData); 163 | } catch (error) { 164 | console.log(error); 165 | } 166 | } 167 | 168 | export async function getQuoteText(paneId: string): Promise { 169 | try { 170 | const data = conf.get(`wezterm.targetPane.pane_${paneId}`); 171 | if (typeof data === "object" && data !== null && "quote_text" in data) { 172 | return String(data.quote_text); 173 | } 174 | return ""; 175 | } catch (error) { 176 | console.log(error); 177 | return ""; 178 | } 179 | } 180 | 181 | export async function clearQuoteText(paneId: string): Promise { 182 | try { 183 | const key = `wezterm.targetPane.pane_${paneId}.quote_text`; 184 | if (conf.has(key)) { 185 | conf.delete(key); 186 | } 187 | } catch (error) { 188 | console.log(error); 189 | } 190 | } 191 | 192 | export async function sendKeyToWeztermPane( 193 | paneId: string, 194 | key: string, 195 | ): Promise { 196 | // Wrap user-provided key in $'...' for bash escape sequences 197 | await execAsync( 198 | `wezterm cli send-text --no-paste --pane-id '${paneId}' $'${key}'`, 199 | ); 200 | } 201 | 202 | export async function inputToWeztermPane( 203 | paneId: string, 204 | content: string, 205 | ): Promise { 206 | // Send content using wezterm cli send-text command (no focus change) 207 | await execAsync( 208 | `wezterm cli send-text --no-paste --pane-id '${paneId}' -- '${content.replace(/'/g, "'\\''")}'`, 209 | ); 210 | console.log(`Content sent to wezterm pane: ${paneId}`); 211 | } 212 | -------------------------------------------------------------------------------- /src/utils/quoteProcessor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Calculate the minimum leading whitespace count across all non-empty lines 3 | */ 4 | function getMinLeadingWhitespace(lines: string[]): number { 5 | let min = 99; 6 | for (const line of lines) { 7 | if (line.length === 0) continue; 8 | const match = line.match(/^[ \t]*/); 9 | const count = match ? match[0].length : 0; 10 | if (count < min) { 11 | min = count; 12 | } 13 | } 14 | return min === 99 ? 0 : min; 15 | } 16 | 17 | /** 18 | * Check if we need a space separator between two lines 19 | */ 20 | function needsSpaceSeparator(prevLine: string, currentLine: string): boolean { 21 | if (prevLine.length === 0 || currentLine.length === 0) { 22 | return false; 23 | } 24 | 25 | const lastChar = prevLine[prevLine.length - 1] ?? ""; 26 | const firstChar = currentLine[0] ?? ""; 27 | 28 | // Check if both are alphabetic (a-z, A-Z) 29 | const isLastCharAlpha = /[a-zA-Z]/.test(lastChar); 30 | const isFirstCharAlpha = /[a-zA-Z]/.test(firstChar); 31 | 32 | return isLastCharAlpha && isFirstCharAlpha; 33 | } 34 | 35 | /** 36 | * Merge lines for Pattern A where indented lines likely indicate wrap 37 | */ 38 | function mergeIndentedContinuations( 39 | originalLines: string[], 40 | trimmedLines: string[], 41 | ): string[] { 42 | const result: string[] = []; 43 | 44 | for (let i = 0; i < trimmedLines.length; i++) { 45 | const line = trimmedLines[i] ?? ""; 46 | const original = originalLines[i] ?? ""; 47 | const prevOriginal = originalLines[i - 1] ?? ""; 48 | 49 | if ( 50 | i > 0 && 51 | original.startsWith(" ") && 52 | original.trimStart().length > 0 && 53 | !prevOriginal.startsWith(" ") && 54 | !/^[-*+]\s/.test(original.trimStart()) // don't merge nested lists 55 | ) { 56 | const prev = result.pop() ?? ""; 57 | const separator = needsSpaceSeparator(prev, line) ? " " : ""; 58 | result.push(prev + separator + line); 59 | continue; 60 | } 61 | 62 | result.push(line); 63 | } 64 | 65 | return result; 66 | } 67 | 68 | /** 69 | * Determine if two lines should be merged 70 | */ 71 | function shouldMergeLines(prevLine: string, currentLine: string): boolean { 72 | // Don't merge if current line starts with Markdown list marker 73 | if (/^[-*+]\s/.test(currentLine)) { 74 | return false; 75 | } 76 | 77 | // Don't merge if both lines contain colons (: or :) 78 | const hasColon = (line: string) => line.includes(":") || line.includes(":"); 79 | if (hasColon(prevLine) && hasColon(currentLine)) { 80 | return false; 81 | } 82 | 83 | // Merge by default 84 | return true; 85 | } 86 | 87 | /** 88 | * Remove common leading whitespace and merge lines 89 | */ 90 | function removeWhitespaceAndMergeLines(lines: string[]): string[] { 91 | // Remove common leading whitespace 92 | const minWhitespace = getMinLeadingWhitespace(lines); 93 | let trimmedLines = lines.map((line) => { 94 | if (line.length === 0) return line; 95 | return line.slice(minWhitespace); 96 | }); 97 | 98 | // Handle wrapped continuation lines even when other lines are not indented. 99 | // Condition: previous line is not indented, current line is indented, and 100 | // current line is not a list item after trimming. 101 | trimmedLines = trimmedLines.map((line, index) => { 102 | const original = lines[index] ?? ""; 103 | const prevOriginal = lines[index - 1] ?? ""; 104 | const isContinuation = 105 | index > 0 && 106 | original.startsWith(" ") && 107 | original.trimStart().length > 0 && 108 | prevOriginal.trim().length > 0 && 109 | !/^[-*+]\s/.test(original.trimStart()); 110 | 111 | if (isContinuation) { 112 | return original.trimStart().trimEnd(); 113 | } 114 | 115 | return line.trimEnd(); 116 | }); 117 | 118 | // Merge lines 119 | const result: string[] = []; 120 | let currentLine = ""; 121 | 122 | for (let i = 0; i < trimmedLines.length; i++) { 123 | const line = trimmedLines[i] ?? ""; 124 | 125 | if (i === 0) { 126 | currentLine = line; 127 | continue; 128 | } 129 | 130 | // Empty lines should always preserve line breaks 131 | if (line.length === 0) { 132 | result.push(currentLine); 133 | result.push(""); 134 | currentLine = ""; 135 | continue; 136 | } 137 | 138 | // If current line is not empty but we have an accumulated empty currentLine from previous empty line 139 | if (currentLine.length === 0) { 140 | currentLine = line; 141 | continue; 142 | } 143 | 144 | const prevLine = trimmedLines[i - 1] ?? ""; 145 | const shouldMerge = shouldMergeLines(prevLine, line); 146 | 147 | if (shouldMerge) { 148 | const separator = needsSpaceSeparator(prevLine, line) ? " " : ""; 149 | currentLine += separator + line; 150 | } else { 151 | result.push(currentLine); 152 | currentLine = line; 153 | } 154 | } 155 | 156 | if (currentLine !== "") { 157 | result.push(currentLine); 158 | } 159 | 160 | return result; 161 | } 162 | 163 | /** 164 | * Processes text for quote buffering by: 165 | * 1. Detecting if 2nd+ lines have no leading whitespace (Pattern A) or all lines have common leading whitespace (Pattern B) 166 | * 2. Pattern A: Remove only leading whitespace, preserve all line breaks 167 | * 3. Pattern B: Remove common leading whitespace and merge lines (with exceptions) 168 | * 4. Adding quote prefix ("> ") to each line 169 | * 5. Adding two newlines at the end 170 | */ 171 | export function processQuoteText( 172 | text: string, 173 | options?: { withQuote?: boolean }, 174 | ): string { 175 | const withQuote = options?.withQuote ?? true; 176 | 177 | // Remove leading and trailing newlines 178 | const trimmedText = text.replace(/^\n+|\n+$/g, ""); 179 | const lines = trimmedText.split("\n"); 180 | 181 | // Pattern detection: Check if any 2nd+ line has no leading whitespace 182 | const hasNoLeadingWhitespaceInLaterLines = lines 183 | .slice(1) 184 | .some( 185 | (line) => 186 | line.length > 0 && !line.startsWith(" ") && !line.startsWith("\t"), 187 | ); 188 | 189 | let processedLines: string[]; 190 | 191 | if (hasNoLeadingWhitespaceInLaterLines) { 192 | // Pattern A: Preserve line breaks, only remove leading whitespace, 193 | // but attempt to merge obviously wrapped continuation lines 194 | const trimmedLines = lines.map((line) => line.trimStart().trimEnd()); 195 | processedLines = mergeIndentedContinuations(lines, trimmedLines); 196 | } else { 197 | // Pattern B: Remove common leading whitespace and merge lines 198 | processedLines = removeWhitespaceAndMergeLines(lines); 199 | } 200 | 201 | // Add quote prefix to each line and join with newlines 202 | if (!withQuote) { 203 | return processedLines.join("\n"); 204 | } 205 | 206 | const quoted = processedLines.map((line) => `> ${line}`).join("\n"); 207 | return `${quoted}\n\n`; 208 | } 209 | -------------------------------------------------------------------------------- /test/modules/editor.test.ts: -------------------------------------------------------------------------------- 1 | import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test"; 2 | import { 3 | getEditor, 4 | launchEditor, 5 | openEditorAndGetContent, 6 | readFileContent, 7 | } from "../../src/modules/editor"; 8 | 9 | // Mock external dependencies 10 | mock.module("node:child_process", () => ({ 11 | spawn: mock(), 12 | })); 13 | 14 | mock.module("node:fs/promises", () => ({ 15 | readFile: mock(), 16 | })); 17 | 18 | mock.module("../../src/utils/tempFile", () => ({ 19 | createTempFile: mock(), 20 | })); 21 | 22 | describe("Editor Module", () => { 23 | beforeEach(() => { 24 | // Reset all mocks 25 | mock.restore(); 26 | }); 27 | 28 | describe("getEditor", () => { 29 | test("should return provided editor option", () => { 30 | const result = getEditor("nvim"); 31 | expect(result).toBe("nvim"); 32 | }); 33 | 34 | test("should return EDITOR environment variable when no option provided", () => { 35 | process.env.EDITOR = "code"; 36 | const result = getEditor(); 37 | expect(result).toBe("code"); 38 | }); 39 | 40 | test("should return default editor when no option or env var", () => { 41 | process.env.EDITOR = undefined; 42 | const result = getEditor(); 43 | expect(result).toBe("vim"); 44 | }); 45 | 46 | test("should prioritize option over environment variable", () => { 47 | process.env.EDITOR = "code"; 48 | const result = getEditor("nvim"); 49 | expect(result).toBe("nvim"); 50 | }); 51 | }); 52 | 53 | describe("launchEditor", () => { 54 | test("should spawn editor process successfully", async () => { 55 | const mockProcess = { 56 | on: mock((event: string, callback): void => { 57 | if (event === "exit") { 58 | // Simulate successful exit 59 | setTimeout(() => callback(0), 10); 60 | } 61 | }), 62 | }; 63 | 64 | const spawnMock = mock(() => mockProcess); 65 | mock.module("node:child_process", () => ({ 66 | spawn: spawnMock, 67 | })); 68 | 69 | expect(launchEditor("vim", "/tmp/test.md")).resolves.toBeUndefined(); 70 | expect(spawnMock).toHaveBeenCalledWith("vim", ["/tmp/test.md"], { 71 | stdio: "inherit", 72 | shell: true, 73 | env: expect.objectContaining({ 74 | EDITPROMPT: "1", 75 | }), 76 | }); 77 | }); 78 | 79 | test("should reject when editor process fails", async () => { 80 | const mockProcess = { 81 | on: mock((event: string, callback) => { 82 | if (event === "error") { 83 | setTimeout(() => callback(new Error("Editor not found")), 10); 84 | } 85 | }), 86 | }; 87 | 88 | const spawnMock = mock(() => mockProcess); 89 | mock.module("node:child_process", () => ({ 90 | spawn: spawnMock, 91 | })); 92 | 93 | expect( 94 | launchEditor("nonexistent-editor", "/tmp/test.md"), 95 | ).rejects.toThrow("Failed to launch editor: Editor not found"); 96 | }); 97 | 98 | test("should reject when editor exits with non-zero code", async () => { 99 | const mockProcess = { 100 | on: mock((event: string, callback) => { 101 | if (event === "exit") { 102 | setTimeout(() => callback(1), 10); 103 | } 104 | }), 105 | }; 106 | 107 | const spawnMock = mock(() => mockProcess); 108 | mock.module("node:child_process", () => ({ 109 | spawn: spawnMock, 110 | })); 111 | 112 | expect(launchEditor("vim", "/tmp/test.md")).rejects.toThrow( 113 | "Editor exited with code: 1", 114 | ); 115 | }); 116 | }); 117 | 118 | describe("readFileContent", () => { 119 | test("should read and trim file content", async () => { 120 | const readFileMock = mock(() => Promise.resolve("Hello World\n")); 121 | mock.module("node:fs/promises", () => ({ 122 | readFile: readFileMock, 123 | })); 124 | 125 | const result = await readFileContent("/tmp/test.md"); 126 | expect(result).toBe("Hello World"); 127 | expect(readFileMock).toHaveBeenCalledWith("/tmp/test.md", "utf-8"); 128 | }); 129 | 130 | test("should throw error when file read fails", async () => { 131 | const readFileMock = mock(() => 132 | Promise.reject(new Error("File not found")), 133 | ); 134 | mock.module("node:fs/promises", () => ({ 135 | readFile: readFileMock, 136 | })); 137 | 138 | expect(readFileContent("/tmp/nonexistent.md")).rejects.toThrow( 139 | "Failed to read file: File not found", 140 | ); 141 | }); 142 | 143 | test("should add space when content ends with @-prefixed string", async () => { 144 | const readFileMock = mock(() => Promise.resolve("foo\n@path/to/file\n")); 145 | mock.module("node:fs/promises", () => ({ 146 | readFile: readFileMock, 147 | })); 148 | 149 | const result = await readFileContent("/tmp/test.md"); 150 | expect(result).toBe("foo\n@path/to/file "); 151 | }); 152 | 153 | test("should add space when line ends with @-prefixed string in middle", async () => { 154 | const readFileMock = mock(() => 155 | Promise.resolve("foo\nbar @path/to/file\n"), 156 | ); 157 | mock.module("node:fs/promises", () => ({ 158 | readFile: readFileMock, 159 | })); 160 | 161 | const result = await readFileContent("/tmp/test.md"); 162 | expect(result).toBe("foo\nbar @path/to/file "); 163 | }); 164 | 165 | test("should not add space when @ appears in middle lines but not at the end", async () => { 166 | const readFileMock = mock(() => 167 | Promise.resolve("foo @path/to/file\nbar\n"), 168 | ); 169 | mock.module("node:fs/promises", () => ({ 170 | readFile: readFileMock, 171 | })); 172 | 173 | const result = await readFileContent("/tmp/test.md"); 174 | expect(result).toBe("foo @path/to/file\nbar"); 175 | }); 176 | }); 177 | 178 | describe("openEditorAndGetContent", () => { 179 | test("should complete full editor workflow successfully", async () => { 180 | const createTempFileMock = mock(() => Promise.resolve("/tmp/test.md")); 181 | mock.module("../../src/utils/tempFile", () => ({ 182 | createTempFile: createTempFileMock, 183 | })); 184 | 185 | const mockProcess = { 186 | on: mock((event: string, callback) => { 187 | if (event === "exit") { 188 | setTimeout(() => callback(0), 10); 189 | } 190 | }), 191 | }; 192 | 193 | const spawnMock = mock(() => mockProcess); 194 | mock.module("node:child_process", () => ({ 195 | spawn: spawnMock, 196 | })); 197 | 198 | const readFileMock = mock(() => Promise.resolve("Test content")); 199 | mock.module("node:fs/promises", () => ({ 200 | readFile: readFileMock, 201 | })); 202 | 203 | const result = await openEditorAndGetContent("vim"); 204 | expect(result).toBe("Test content"); 205 | expect(createTempFileMock).toHaveBeenCalled(); 206 | expect(spawnMock).toHaveBeenCalledWith("vim", ["/tmp/test.md"], { 207 | stdio: "inherit", 208 | shell: true, 209 | env: expect.objectContaining({ 210 | EDITPROMPT: "1", 211 | }), 212 | }); 213 | expect(readFileMock).toHaveBeenCalledWith("/tmp/test.md", "utf-8"); 214 | }); 215 | 216 | test("should throw error when no content is entered", async () => { 217 | const createTempFileMock = mock(() => Promise.resolve("/tmp/test.md")); 218 | mock.module("../../src/utils/tempFile", () => ({ 219 | createTempFile: createTempFileMock, 220 | })); 221 | 222 | const mockProcess = { 223 | on: mock((event: string, callback) => { 224 | if (event === "exit") { 225 | setTimeout(() => callback(0), 10); 226 | } 227 | }), 228 | }; 229 | 230 | const spawnMock = mock(() => mockProcess); 231 | mock.module("node:child_process", () => ({ 232 | spawn: spawnMock, 233 | })); 234 | 235 | const readFileMock = mock(() => Promise.resolve("")); 236 | mock.module("node:fs/promises", () => ({ 237 | readFile: readFileMock, 238 | })); 239 | 240 | const result = await openEditorAndGetContent("vim"); 241 | expect(result).toBe(""); 242 | }); 243 | }); 244 | }); 245 | -------------------------------------------------------------------------------- /docs/modes.md: -------------------------------------------------------------------------------- 1 | # Subcommand Implementation Details 2 | 3 | This document provides technical details about how each subcommand works, including constraints and solutions for different terminal multiplexers. 4 | 5 | ## Available Subcommands 6 | 7 | - open 8 | - register 9 | - resume 10 | - input 11 | - collect 12 | - dump 13 | 14 | --- 15 | 16 | ## open Subcommand 17 | 18 | ### Purpose 19 | 20 | Launches an editor, waits for content to be written, and sends it to the target pane when the editor closes. 21 | 22 | ### Multiple Target Panes Support 23 | 24 | The `--target-pane` option can be specified multiple times. Content is sent sequentially to all specified panes. 25 | 26 | ```bash 27 | # Example: sending to multiple panes 28 | editprompt open --target-pane %1 --target-pane %2 --target-pane %3 29 | ``` 30 | 31 | - If all panes receive content successfully: exit code 0 32 | - If some or all panes fail to receive content: exit code 1 33 | 34 | ### Constraints and Solutions 35 | 36 | #### tmux with `run-shell` 37 | - **Constraint**: `run-shell` creates a new shell process that does not inherit environment variables like `$EDITOR` from the parent shell 38 | - **Solution**: Explicitly specify the editor using `--editor nvim` 39 | 40 | ```tmux 41 | # Environment variables are not inherited, so explicitly specify the editor 42 | bind -n M-q run-shell 'editprompt open --editor nvim --target-pane #{pane_id}' 43 | ``` 44 | 45 | #### WezTerm with `SplitPane` 46 | - **Constraint**: `SplitPane` performs simple command execution without loading login shell configuration (PATH, etc.) 47 | - **Solution**: Explicitly launch a login shell with `/bin/zsh -lc "editprompt ..."` to load environment variables and PATH 48 | 49 | ```lua 50 | -- Use -lc flag to launch as login shell and load environment variables and PATH 51 | SplitPane({ 52 | command = { 53 | args = { 54 | "/bin/zsh", 55 | "-lc", 56 | "editprompt open --editor nvim --target-pane " .. target_pane_id, 57 | }, 58 | }, 59 | }) 60 | ``` 61 | 62 | --- 63 | 64 | ## register Subcommand 65 | 66 | ### Purpose 67 | 68 | Registers the relationship between editor panes and target panes. Enables bidirectional focus switching in resume mode and content delivery in input mode. 69 | 70 | ### Usage Examples 71 | 72 | ```bash 73 | # Register target panes to the current pane (editor pane) 74 | editprompt register --target-pane %1 --target-pane %2 75 | 76 | # Explicitly specify editor pane ID for registration 77 | editprompt register --editor-pane %10 --target-pane %1 --target-pane %2 78 | ``` 79 | 80 | ### Behavior 81 | 82 | 1. **Determine editor pane**: 83 | - If `--editor-pane` option is specified: Use that ID 84 | - If not specified: Use the current pane as the editor pane 85 | - Error if the current pane is not an editor pane 86 | 87 | 2. **Merge with existing target panes**: 88 | - Retrieve existing target pane IDs already registered to the editor pane 89 | - Merge with newly specified target pane IDs 90 | - Remove duplicates to create a unique array 91 | 92 | 3. **Save relationship**: 93 | - **tmux**: Save as comma-separated values in `@editprompt_target_panes` pane variable 94 | - **WezTerm**: Save `targetPaneIds` as an array using Conf library 95 | 96 | --- 97 | 98 | ## resume Subcommand 99 | 100 | ### Purpose 101 | 102 | Reuses existing editor panes and enables bidirectional focus switching between target and editor panes. 103 | 104 | ### Constraints and Solutions 105 | 106 | This mode needs to maintain the relationship between editor panes and target panes, enabling bidirectional focus switching. 107 | 108 | #### tmux Implementation 109 | - **Solution**: Use tmux pane variables (`@variable`) to persist data 110 | - **Variables used**: 111 | - `@editprompt_editor_pane`: Set on target pane, stores editor pane ID 112 | - `@editprompt_is_editor`: Set on editor pane, flag indicating it's an editor pane 113 | - `@editprompt_target_panes`: Set on editor pane, stores original target pane IDs as comma-separated values 114 | 115 | ```bash 116 | # Save editor pane ID on target pane 117 | tmux set-option -pt '${targetPaneId}' @editprompt_editor_pane '${editorPaneId}' 118 | 119 | # Save flag and multiple target pane IDs on editor pane 120 | tmux set-option -pt '${editorPaneId}' @editprompt_is_editor 1 121 | tmux set-option -pt '${editorPaneId}' @editprompt_target_panes '${targetPaneId1},${targetPaneId2}' 122 | ``` 123 | 124 | When switching back from the editor pane, it focuses on the first existing pane among the saved target pane IDs (retry logic). 125 | 126 | While tmux's `run-shell` does not inherit environment variables, it can access pane variables like `#{pane_id}`, making this approach work seamlessly. 127 | 128 | #### WezTerm Implementation 129 | - **Constraint**: WezTerm's `run_child_process` does not inherit environment variables or PATH, requiring execution via `/bin/zsh -lc` 130 | - **Constraint**: WezTerm does not have a pane variable mechanism like tmux 131 | - **About OSC User Variables**: WezTerm supports [user variable definition via OSC 1337](https://wezfurlong.org/wezterm/shell-integration.html#user-vars), but this method **cannot be used for panes running active processes** 132 | - OSC user variables are meant to be set during shell prompt display and similar events; they cannot be set externally on target panes already running processes like Claude Code 133 | - Editor panes also run processes like nvim after launch, facing the same constraint 134 | - **Solution**: Use the [Conf](https://github.com/sindresorhus/conf) library to persist data to the filesystem 135 | - `wezterm.targetPane.pane_${targetPaneId}`: Stores editor pane ID 136 | - `wezterm.editorPane.pane_${editorPaneId}`: Stores multiple target pane IDs as an array 137 | 138 | ```typescript 139 | // Use Conf library to save the relationship between target and editor panes 140 | conf.set(`wezterm.targetPane.pane_${targetPaneId}`, { 141 | editorPaneId: editorPaneId, 142 | }); 143 | conf.set(`wezterm.editorPane.pane_${editorPaneId}`, { 144 | targetPaneIds: [targetPaneId1, targetPaneId2], 145 | }); 146 | ``` 147 | 148 | When switching back from the editor pane, it focuses on the first existing target pane, similar to tmux. 149 | 150 | This approach enables bidirectional focus switching in WezTerm, similar to tmux. 151 | 152 | --- 153 | 154 | ## input Subcommand 155 | 156 | ### Purpose 157 | 158 | Sends content to the target pane without opening an editor, designed to be executed from within an editor session. 159 | 160 | ### Mechanism 161 | 162 | This mode is designed to be executed from within the editor, reading configuration from both environment variables and pane variables/Conf. 163 | 164 | #### Workflow 165 | 1. **When launching the editor in open subcommand**: 166 | - The following environment variables are set when launching the editor: 167 | - `EDITPROMPT_MUX`: Multiplexer to use (`tmux` or `wezterm`) 168 | - `EDITPROMPT_ALWAYS_COPY`: Clipboard copy configuration 169 | - `EDITPROMPT=1`: Flag indicating launched by editprompt 170 | - Target pane IDs are stored in pane variables or Conf: 171 | - tmux: `@editprompt_target_panes` (comma-separated) 172 | - wezterm: `targetPaneIds` (array) 173 | 174 | 2. **When executing input subcommand from within the editor**: 175 | - Execute `editprompt input -- "content"` from editors like Neovim 176 | - Inherits environment variables from the parent process (editor) 177 | - Gets the current pane ID and reads multiple target pane IDs from pane variables/Conf 178 | - Sends content sequentially to each target pane 179 | 180 | ```lua 181 | -- Example execution from Neovim 182 | vim.system( 183 | { "editprompt", "input", "--", content }, 184 | { text = true }, 185 | function(obj) 186 | -- Environment variables are properly inherited when executed from the editor 187 | end 188 | ) 189 | ``` 190 | 191 | ### Benefits 192 | - Send content multiple times without closing the editor 193 | - Environment variable inheritance happens naturally, requiring no additional configuration or arguments 194 | - Supports sending to multiple target panes 195 | 196 | --- 197 | 198 | ## collect Subcommand 199 | 200 | ### Purpose 201 | 202 | Collects text selections and stores them as quoted text (with `> ` prefix) in pane variables or persistent storage. Used to accumulate multiple selections for later retrieval with dump subcommand. 203 | 204 | ### Mechanism 205 | 206 | This mode enables collecting multiple text selections while reading AI responses or terminal output, preparing them for a reply with context. 207 | 208 | #### Text Input Methods 209 | 210 | **tmux Implementation:** 211 | - Reads text from stdin using pipe in copy mode 212 | - Example: `bind-key -T copy-mode-vi C-e { send-keys -X pipe "editprompt collect --target-pane #{pane_id}" }` 213 | - Example (also send cleaned text to clipboard without quote prefix): 214 | `bind-key -T copy-mode-vi y { send-keys -X pipe "editprompt collect --target-pane #{pane_id} --output buffer --output stdout --no-quote | pbcopy" }` 215 | 216 | **WezTerm Implementation:** 217 | - Receives text as a positional argument 218 | - Example: `editprompt collect --mux wezterm --target-pane -- ""` 219 | - Uses `wezterm.shell_quote_arg()` for proper escaping 220 | 221 | #### Text Processing 222 | 223 | The `processQuoteText` function applies intelligent text formatting: 224 | 225 | 1. **Remove leading/trailing newlines**: Cleans up selection boundaries 226 | 2. **Pattern Detection**: Analyzes indentation structure 227 | - **Pattern A** (No leading whitespace in 2nd+ lines): Preserves all line breaks, removes only leading whitespace 228 | - **Pattern B** (Common leading whitespace): Removes common indentation and merges lines with exceptions: 229 | - Never merges lines starting with Markdown list markers (`-`, `*`, `+`) 230 | - Never merges when both lines contain colons (`:` or `:`) 231 | - Adds space separator between lines only when both end/start with alphabetic characters 232 | 3. **Add quote prefix**: Prepends `> ` to each line 233 | 4. **Add trailing newlines**: Appends two newlines for separation between multiple quotes 234 | 5. **Disable quote formatting when needed**: Use `--no-quote` to skip adding `> ` and the trailing blank lines (indent/newline cleanup still applies) 235 | 236 | #### Output Destinations 237 | 238 | - Default: `--output buffer` (store in tmux pane variable / WezTerm Conf) 239 | - Tee to stdout: `--output buffer --output stdout` to pipe the same processed text to another command (e.g., clipboard) 240 | - `--no-quote` applies to all outputs, producing cleaned text without Markdown quote prefix or trailing blank lines 241 | 242 | #### Storage Implementation 243 | 244 | **tmux Implementation:** 245 | - Stores accumulated quotes in `@editprompt_quote` pane variable 246 | - Appends new quotes to existing content with newline separator 247 | - Single quote escaping: Uses `'\''` pattern for shell safety 248 | 249 | ```bash 250 | # Append to existing quote content 251 | tmux set-option -pt '${paneId}' @editprompt_quote '${newContent}' 252 | ``` 253 | 254 | **WezTerm Implementation:** 255 | - Stores quotes in Conf library under `wezterm.targetPane.pane_${paneId}.quote_text` 256 | - Appends new quotes to existing content with newline separator 257 | 258 | ```typescript 259 | // Append to existing quote content 260 | const existingQuotes = conf.get(`wezterm.targetPane.pane_${paneId}.quote_text`) || ''; 261 | const newQuotes = existingQuotes + '\n' + content; 262 | conf.set(`wezterm.targetPane.pane_${paneId}.quote_text`, newQuotes); 263 | ``` 264 | 265 | ### Benefits 266 | - Collect multiple selections from long AI responses or terminal output 267 | - Intelligent text processing removes formatting artifacts 268 | - Quotes are automatically formatted in Markdown quote style 269 | - Persistent storage survives across multiple selections 270 | 271 | --- 272 | 273 | ## dump Subcommand 274 | 275 | ### Purpose 276 | 277 | Retrieves all accumulated quoted text from collect subcommand and outputs it to stdout, then clears the storage. Designed to be executed from within an editor session to insert collected quotes. 278 | 279 | ### Mechanism 280 | 281 | This mode is designed to work with the collect subcommand workflow, retrieving accumulated selections for editing and replying. 282 | 283 | #### Configuration Source 284 | 285 | Unlike collect subcommand which requires `--target-pane` argument, dump subcommand reads configuration from environment variables and pane variables/Conf: 286 | - `EDITPROMPT_MUX`: Multiplexer type (`tmux` or `wezterm`) 287 | - Multiple target pane IDs retrieved from current pane ID: 288 | - tmux: `@editprompt_target_panes` (comma-separated) 289 | - wezterm: `targetPaneIds` (array) 290 | 291 | These configurations are automatically set when launching the editor in open subcommand. 292 | 293 | #### Workflow 294 | 295 | 1. **Read environment variables**: Gets multiplexer type from environment 296 | 2. **Get current pane ID**: Identifies the editor pane ID 297 | 3. **Get target pane IDs**: Retrieves multiple target pane IDs from pane variables/Conf 298 | 4. **Retrieve quote content**: Fetches accumulated quotes from all target panes 299 | - **tmux**: Reads from `@editprompt_quote` pane variable 300 | - **WezTerm**: Reads from `wezterm.targetPane.pane_${paneId}.quote_text` in Conf 301 | 5. **Clear storage**: Removes quote content from storage for each target pane after retrieval 302 | - **tmux**: Sets `@editprompt_quote` to empty string 303 | - **WezTerm**: Deletes `quote_text` key from Conf 304 | 6. **Combine and output**: Joins all quotes with newlines and writes to stdout with trailing newline cleanup (max 2 newlines) 305 | 306 | ```typescript 307 | // Combine and output with cleaned trailing newlines 308 | const combinedContent = quoteContents.join("\n"); 309 | process.stdout.write(combinedContent.replace(/\n{3,}$/, "\n\n")); 310 | ``` 311 | 312 | #### Storage Retrieval 313 | 314 | **tmux Implementation:** 315 | ```bash 316 | # Get quote content 317 | tmux show -pt '${paneId}' -v @editprompt_quote 318 | 319 | # Clear quote content 320 | tmux set-option -pt '${paneId}' @editprompt_quote "" 321 | ``` 322 | 323 | **WezTerm Implementation:** 324 | ```typescript 325 | // Get quote content 326 | const data = conf.get(`wezterm.targetPane.pane_${paneId}`); 327 | const quoteContent = data?.quote_text || ''; 328 | 329 | // Clear quote content 330 | conf.delete(`wezterm.targetPane.pane_${paneId}.quote_text`); 331 | ``` 332 | 333 | ### Benefits 334 | - Retrieves all accumulated quotes in a single command 335 | - Automatically combines quotes from multiple target panes 336 | - Automatically clears storage for next collection session 337 | - Works seamlessly from within editor via environment variable inheritance 338 | - No need to specify target pane or multiplexer type manually 339 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | link to npm.js 3 |

4 | 5 | # 📝 editprompt 6 | 7 | A CLI tool that lets you write prompts for CLI tools using your favorite text editor. Works seamlessly with Claude Code, Codex CLI, Gemini CLI, and any other CLI process. 8 | 9 | ![send without closing editor](https://github.com/user-attachments/assets/b0e486af-78d7-4b70-8c82-64d330c22ba1) 10 | 11 | > [!IMPORTANT] 12 | > **📢 Migrating from v0.8.1 or earlier?** Please see the [Migration Guide](docs/migration-guide-v1.md) for upgrading to v1.0.0's subcommand-based interface. 13 | 14 | ## 🏆 Why editprompt? 15 | 16 | - **🎯 Your Editor, Your Way**: Write prompts in your favorite editor with full syntax highlighting, plugins, and customizations 17 | - **🚫 No Accidental Sends**: Never accidentally hit Enter and send an incomplete prompt again 18 | - **🔄 Iterate Efficiently**: Keep your editor open and send multiple prompts without reopening 19 | - **💬 Quote and Reply**: Collect multiple text selections and reply to specific parts of AI responses 20 | - **📝 Multi-line Commands**: Complex SQL queries, JSON payloads, and structured prompts 21 | 22 | 23 | ## ✨ Features 24 | 25 | - 🖊️ **Editor Integration**: Use your preferred text editor to write prompts 26 | - 🖥️ **Multiplexer Support**: Send prompts directly to tmux or WezTerm sessions 27 | - 🖥️ **Universal Terminal Support**: Works with any terminal via clipboard - no multiplexer required 28 | - 📤 **Send Without Closing**: Iterate on prompts without closing your editor 29 | - 📋 **Quote Buffering**: Collect text selections and send them as quoted replies 30 | - 📋 **Clipboard Fallback**: Automatically copies to clipboard if sending fails 31 | 32 | 33 | ## 📦 Installation 34 | 35 | ```bash 36 | # Install globally via npm 37 | npm install -g editprompt 38 | 39 | # Or use with npx 40 | npx editprompt 41 | ``` 42 | 43 | ## 🚀 Usage 44 | 45 | editprompt supports three main workflows to fit different use cases: 46 | 47 | ### Workflow 1: Basic - Write and Send 48 | 49 | ![wrihte and send prompt by editprompt](https://github.com/user-attachments/assets/6587b0c4-8132-4d5c-be68-3aa32a8d4df2) 50 | 51 | The simplest way to use editprompt: 52 | 53 | 1. Run `editprompt open` to open your editor 54 | 2. Write your prompt 55 | 3. Save and close the editor 56 | 4. Content is automatically sent to the target pane or clipboard 57 | 58 | Perfect for one-off prompts when you need more space than a terminal input line. 59 | 60 | ### Workflow 2: Interactive - Iterate with Editor Open 61 | 62 | ![send without closing editor](https://github.com/user-attachments/assets/b0e486af-78d7-4b70-8c82-64d330c22ba1) 63 | 64 | For iterating on prompts without constantly reopening the editor: 65 | 66 | 1. Set up a keybinding to open editprompt with `resume` subcommand 67 | 2. Editor pane stays open between sends 68 | 3. Write, send, refine, send again - all without closing the editor 69 | 4. Use the same keybinding to toggle between your work pane and editor pane 70 | 71 | Ideal for trial-and-error workflows with AI assistants. 72 | 73 | ### Workflow 3: Quote - Collect and Reply 74 | 75 | ![quote and capture with editprompt](https://github.com/user-attachments/assets/33af0702-5c80-4ccf-80d9-0ae42052e6fa) 76 | 77 | ```markdown 78 | > Some AI agents include leading spaces in their output,which can make the copied text look a bit awkward. 79 | 80 | 81 | 82 | > Using editprompt’s quote mode or capture mode makes it easy to reply while quoting the AI agent’s output. 83 | 84 | 85 | ``` 86 | 87 | For replying to specific parts of AI responses: 88 | 89 | 1. Select text in your terminal (tmux copy mode or WezTerm selection) and trigger collect mode 90 | 2. Repeat to collect multiple selections 91 | 3. Run `editprompt dump` to retrieve all collected quotes 92 | 4. Edit and send your reply with context 93 | 94 | Perfect for addressing multiple points in long AI responses. 95 | 96 | 97 | ## ⚙️ Setup & Configuration 98 | 99 | ### Basic Setup 100 | 101 | ```bash 102 | # Use with your default editor (from $EDITOR) 103 | editprompt open 104 | 105 | # Specify a different editor 106 | editprompt open --editor nvim 107 | editprompt open -e nvim 108 | 109 | # Always copy to clipboard 110 | editprompt open --always-copy 111 | 112 | # Show help 113 | editprompt --help 114 | editprompt open --help 115 | ``` 116 | 117 | ### Tmux Integration 118 | 119 | ```tmux 120 | bind -n M-q run-shell '\ 121 | editprompt resume --target-pane #{pane_id} || \ 122 | tmux split-window -v -l 10 -c "#{pane_current_path}" \ 123 | "editprompt open --editor nvim --always-copy --target-pane #{pane_id}"' 124 | ``` 125 | 126 | 127 | ### WezTerm Integration 128 | 129 | ```lua 130 | { 131 | key = "q", 132 | mods = "OPT", 133 | action = wezterm.action_callback(function(window, pane) 134 | local target_pane_id = tostring(pane:pane_id()) 135 | 136 | -- Try to resume existing editor pane 137 | local success, stdout, stderr = wezterm.run_child_process({ 138 | "/bin/zsh", 139 | "-lc", 140 | string.format( 141 | "editprompt resume --mux wezterm --target-pane %s", 142 | target_pane_id 143 | ), 144 | }) 145 | 146 | -- If resume failed, create new editor pane 147 | if not success then 148 | window:perform_action( 149 | act.SplitPane({ 150 | direction = "Down", 151 | size = { Cells = 10 }, 152 | command = { 153 | args = { 154 | "/bin/zsh", 155 | "-lc", 156 | string.format( 157 | "editprompt open --editor nvim --always-copy --mux wezterm --target-pane %s", 158 | target_pane_id 159 | ), 160 | }, 161 | }, 162 | }), 163 | pane 164 | ) 165 | end 166 | end), 167 | }, 168 | ``` 169 | 170 | **Note:** The `-lc` flag ensures your shell loads the full login environment, making `editprompt` available in your PATH. 171 | 172 | 173 | ### Editor Integration (Send Without Closing) 174 | 175 | While editprompt is running, you can send content to the target pane or clipboard without closing the editor. This allows you to iterate quickly on your prompts. 176 | 177 | #### Command Line Usage 178 | 179 | ```bash 180 | # Run this command from within your editor session 181 | editprompt input -- "your content here" 182 | # Sends content to target pane and moves focus there 183 | 184 | editprompt input --auto-send -- "your content here" 185 | # Sends content, automatically submits it (presses Enter), and returns focus to editor pane 186 | # Perfect for iterating on prompts without leaving your editor 187 | 188 | editprompt input --auto-send --send-key "C-m" -- "your content here" 189 | # Customize the key to send after content (tmux format example) 190 | # WezTerm example: --send-key "\r" (default for WezTerm is \r, tmux default is Enter) 191 | ``` 192 | 193 | This sends the content to the target pane (or clipboard) while keeping your editor open, so you can continue editing and send multiple times. 194 | 195 | **Options:** 196 | - `--auto-send`: Automatically sends the content and returns focus to your editor pane (requires multiplexer) 197 | - `--send-key `: Customize the key to send after content (requires `--auto-send`) 198 | - tmux format: `Enter` (default), `C-a`, etc. 199 | - WezTerm format: `\r` (default), `\x01`, etc. 200 | 201 | #### Neovim Integration Example 202 | 203 | You can set up a convenient keybinding to send your buffer content: 204 | 205 | ```lua 206 | -- Send buffer content while keeping the editor open 207 | if vim.env.EDITPROMPT then 208 | vim.keymap.set("n", "x", function() 209 | vim.cmd("update") 210 | -- Get buffer content 211 | local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) 212 | local content = table.concat(lines, "\n") 213 | 214 | -- Execute editprompt command 215 | vim.system( 216 | { "editprompt", "input", "--", content }, 217 | { text = true }, 218 | function(obj) 219 | vim.schedule(function() 220 | if obj.code == 0 then 221 | -- Clear buffer on success 222 | vim.api.nvim_buf_set_lines(0, 0, -1, false, {}) 223 | vim.cmd("silent write") 224 | else 225 | -- Show error notification 226 | vim.notify("editprompt failed: " .. (obj.stderr or "unknown error"), vim.log.levels.ERROR) 227 | end 228 | end) 229 | end 230 | ) 231 | end, { silent = true, desc = "Send buffer content to editprompt" }) 232 | end 233 | ``` 234 | 235 | **Usage:** 236 | 1. Open editprompt using the tmux/wezterm keybinding 237 | 2. Write your prompt in the editor 238 | 3. Press `x` to send the content to the target pane 239 | 4. The buffer is automatically cleared on success 240 | 5. Continue editing to send more content 241 | 242 | ### Quote Workflow Setup 243 | 244 | #### Collecting Quotes in tmux Copy Mode 245 | 246 | Add this keybinding to your `.tmux.conf` to collect selected text as quotes: 247 | 248 | ```tmux 249 | bind-key -T copy-mode-vi C-e { send-keys -X pipe "editprompt collect --target-pane #{pane_id}" } 250 | ``` 251 | 252 | **Usage:** 253 | 1. Enter tmux copy mode (`prefix + [`) 254 | 2. Select text using vi-mode keybindings 255 | 3. Press `Ctrl-e` to add the selection as a quote 256 | 4. Repeat to collect multiple quotes 257 | 5. All quotes are stored in a pane variable associated with the target pane 258 | 259 | #### Collecting Quotes in WezTerm 260 | 261 | Add this event handler and keybinding to your `wezterm.lua` to collect selected text as quotes: 262 | 263 | ```lua 264 | local wezterm = require("wezterm") 265 | 266 | wezterm.on("editprompt-collect", function(window, pane) 267 | local text = window:get_selection_text_for_pane(pane) 268 | local target_pane_id = tostring(pane:pane_id()) 269 | 270 | wezterm.run_child_process({ 271 | "/bin/zsh", 272 | "-lc", 273 | string.format( 274 | "editprompt collect --mux wezterm --target-pane %s -- %s", 275 | target_pane_id, 276 | wezterm.shell_quote_arg(text) 277 | ), 278 | }) 279 | end) 280 | 281 | return { 282 | keys = { 283 | { 284 | key = "e", 285 | mods = "CTRL", 286 | action = wezterm.action.EmitEvent("editprompt-collect"), 287 | }, 288 | }, 289 | } 290 | ``` 291 | 292 | **Usage:** 293 | 1. Select text in WezTerm (by dragging with mouse or using copy mode) 294 | 2. Press `Ctrl-e` to add the selection as a quote 295 | 3. Repeat to collect multiple quotes 296 | 4. All quotes are stored in a configuration file associated with the target pane 297 | 298 | #### Capturing Collected Quotes 299 | 300 | Run this command from within your editor pane to retrieve all collected quotes: 301 | 302 | ```bash 303 | editprompt dump 304 | ``` 305 | 306 | This copies all collected quotes to the clipboard and clears the buffer, ready for your reply. 307 | 308 | **Complete workflow:** 309 | 1. AI responds with multiple points 310 | 2. Select each point in copy mode and press `Ctrl-e` 311 | 3. Open your editor pane and run `editprompt dump` 312 | 4. Edit the quoted text with your responses 313 | 5. Send to AI 314 | 315 | **How quote buffering works:** 316 | - **tmux**: Quotes are stored in pane variables, automatically cleaned up when the pane closes 317 | - **WezTerm**: Quotes are stored in a configuration file associated with the pane 318 | - Text is intelligently processed: removes common indentation, handles line breaks smartly 319 | - Each quote is prefixed with `> ` in markdown quote format 320 | - Multiple quotes are separated with blank lines 321 | 322 | ### Sending to Multiple Panes 323 | 324 | You can send content to multiple target panes simultaneously by specifying `--target-pane` multiple times: 325 | 326 | ```bash 327 | # Send to multiple panes with open subcommand 328 | editprompt open --target-pane %1 --target-pane %2 --target-pane %3 329 | 330 | # Register multiple target panes for use with resume and input modes 331 | editprompt register --target-pane %1 --target-pane %2 332 | ``` 333 | 334 | The content will be sent sequentially to all specified panes. This is useful when you want to send the same prompt to multiple CLI sessions. 335 | 336 | #### Neovim Integration Example 337 | 338 | You can set up a convenient keybinding to capture your quote content: 339 | ```lua 340 | vim.keymap.set("n", "X", function() 341 | vim.cmd("update") 342 | 343 | vim.system({ "editprompt", "dump" }, { text = true }, function(obj) 344 | vim.schedule(function() 345 | if obj.code == 0 then 346 | vim.cmd("silent write") 347 | -- Split stdout by lines 348 | local output_lines = vim.split(obj.stdout, "\n") 349 | 350 | local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) 351 | local is_empty = #lines == 1 and lines[1] == "" 352 | 353 | if is_empty then 354 | -- If empty, overwrite from the beginning 355 | vim.api.nvim_buf_set_lines(0, 0, -1, false, output_lines) 356 | vim.cmd("normal 2j") 357 | else 358 | -- If not empty, append to the end 359 | table.insert(output_lines, 1, "") 360 | local line_count = vim.api.nvim_buf_line_count(0) 361 | vim.api.nvim_buf_set_lines( 362 | 0, 363 | line_count, 364 | line_count, 365 | false, 366 | output_lines 367 | ) 368 | vim.cmd("normal 4j") 369 | end 370 | 371 | vim.cmd("silent write") 372 | else 373 | vim.notify( 374 | "editprompt failed: " .. (obj.stderr or "unknown error"), 375 | vim.log.levels.ERROR 376 | ) 377 | end 378 | end) 379 | end) 380 | end, { silent = true, desc = "Capture from editprompt quote mode" }) 381 | ``` 382 | 383 | ### Environment Variables 384 | 385 | #### Editor Selection 386 | 387 | editprompt respects the following editor priority: 388 | 389 | 1. `--editor/-e` command line option 390 | 2. `$EDITOR` environment variable 391 | 3. Default: `vim` 392 | 393 | #### EDITPROMPT Environment Variable 394 | 395 | editprompt automatically sets `EDITPROMPT=1` when launching your editor. This allows you to detect when your editor is launched by editprompt and enable specific configurations or plugins. 396 | 397 | **Example: Neovim Configuration** 398 | 399 | ```lua 400 | -- In your Neovim config (e.g., init.lua) 401 | if vim.env.EDITPROMPT then 402 | vim.opt.wrap = true 403 | -- Load a specific colorscheme 404 | vim.cmd('colorscheme blue') 405 | end 406 | ``` 407 | 408 | #### Custom Environment Variables 409 | 410 | You can also pass custom environment variables to your editor: 411 | 412 | ```bash 413 | # Single environment variable 414 | editprompt open --env THEME=dark 415 | 416 | # Multiple environment variables 417 | editprompt open --env THEME=dark --env FOO=fooooo 418 | 419 | # Useful for editor-specific configurations 420 | editprompt open --env NVIM_CONFIG=minimal 421 | ``` 422 | 423 | #### Target Pane Environment Variable 424 | 425 | When using the send-without-closing feature or dump, editprompt sets `EDITPROMPT_TARGET_PANE` to the target pane ID. This is automatically used by `editprompt input` and `editprompt dump` commands. 426 | -------------------------------------------------------------------------------- /bun.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1, 3 | "configVersion": 0, 4 | "workspaces": { 5 | "": { 6 | "name": "editprompt", 7 | "dependencies": { 8 | "clipboardy": "^4.0.0", 9 | "conf": "^15.0.2", 10 | "gunshi": "v0.27.0-beta.3", 11 | }, 12 | "devDependencies": { 13 | "@biomejs/biome": "1.9.4", 14 | "@types/bun": "^1.3.2", 15 | "@types/node": "^24.10.1", 16 | "@typescript/native-preview": "^7.0.0-dev.20251119.1", 17 | "bumpp": "^10.3.1", 18 | "tsdown": "latest", 19 | }, 20 | "peerDependencies": { 21 | "typescript": "^5.8.3", 22 | }, 23 | }, 24 | }, 25 | "packages": { 26 | "@babel/generator": ["@babel/generator@7.28.5", "", { "dependencies": { "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ=="], 27 | 28 | "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], 29 | 30 | "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], 31 | 32 | "@babel/parser": ["@babel/parser@7.28.5", "", { "dependencies": { "@babel/types": "^7.28.5" }, "bin": "./bin/babel-parser.js" }, "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ=="], 33 | 34 | "@babel/types": ["@babel/types@7.28.5", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA=="], 35 | 36 | "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], 37 | 38 | "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], 39 | 40 | "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="], 41 | 42 | "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="], 43 | 44 | "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="], 45 | 46 | "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="], 47 | 48 | "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="], 49 | 50 | "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="], 51 | 52 | "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], 53 | 54 | "@emnapi/core": ["@emnapi/core@1.6.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-zq/ay+9fNIJJtJiZxdTnXS20PllcYMX3OE23ESc4HK/bdYu3cOWYVhsOhVnXALfU/uqJIxn5NBPd9z4v+SfoSg=="], 55 | 56 | "@emnapi/runtime": ["@emnapi/runtime@1.6.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA=="], 57 | 58 | "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], 59 | 60 | "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], 61 | 62 | "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], 63 | 64 | "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], 65 | 66 | "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], 67 | 68 | "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" } }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="], 69 | 70 | "@oxc-project/runtime": ["@oxc-project/runtime@0.96.0", "", {}, "sha512-34lh4o9CcSw09Hx6fKihPu85+m+4pmDlkXwJrLvN5nMq5JrcGhhihVM415zDqT8j8IixO1PYYdQZRN4SwQCncg=="], 71 | 72 | "@oxc-project/types": ["@oxc-project/types@0.97.0", "", {}, "sha512-lxmZK4xFrdvU0yZiDwgVQTCvh2gHWBJCBk5ALsrtsBWhs0uDIi+FTOnXRQeQfs304imdvTdaakT/lqwQ8hkOXQ=="], 73 | 74 | "@quansync/fs": ["@quansync/fs@0.1.5", "", { "dependencies": { "quansync": "^0.2.11" } }, "sha512-lNS9hL2aS2NZgNW7BBj+6EBl4rOf8l+tQ0eRY6JWCI8jI2kc53gSoqbjojU0OnAWhzoXiOjFyGsHcDGePB3lhA=="], 75 | 76 | "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-beta.50", "", { "os": "android", "cpu": "arm64" }, "sha512-XlEkrOIHLyGT3avOgzfTFSjG+f+dZMw+/qd+Y3HLN86wlndrB/gSimrJCk4gOhr1XtRtEKfszpadI3Md4Z4/Ag=="], 77 | 78 | "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-beta.50", "", { "os": "darwin", "cpu": "arm64" }, "sha512-+JRqKJhoFlt5r9q+DecAGPLZ5PxeLva+wCMtAuoFMWPoZzgcYrr599KQ+Ix0jwll4B4HGP43avu9My8KtSOR+w=="], 79 | 80 | "@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-beta.50", "", { "os": "darwin", "cpu": "x64" }, "sha512-fFXDjXnuX7/gQZQm/1FoivVtRcyAzdjSik7Eo+9iwPQ9EgtA5/nB2+jmbzaKtMGG3q+BnZbdKHCtOacmNrkIDA=="], 81 | 82 | "@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-beta.50", "", { "os": "freebsd", "cpu": "x64" }, "sha512-F1b6vARy49tjmT/hbloplzgJS7GIvwWZqt+tAHEstCh0JIh9sa8FAMVqEmYxDviqKBaAI8iVvUREm/Kh/PD26Q=="], 83 | 84 | "@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.50", "", { "os": "linux", "cpu": "arm" }, "sha512-U6cR76N8T8M6lHj7EZrQ3xunLPxSvYYxA8vJsBKZiFZkT8YV4kjgCO3KwMJL0NOjQCPGKyiXO07U+KmJzdPGRw=="], 85 | 86 | "@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-beta.50", "", { "os": "linux", "cpu": "arm64" }, "sha512-ONgyjofCrrE3bnh5GZb8EINSFyR/hmwTzZ7oVuyUB170lboza1VMCnb8jgE6MsyyRgHYmN8Lb59i3NKGrxrYjw=="], 87 | 88 | "@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-beta.50", "", { "os": "linux", "cpu": "arm64" }, "sha512-L0zRdH2oDPkmB+wvuTl+dJbXCsx62SkqcEqdM+79LOcB+PxbAxxjzHU14BuZIQdXcAVDzfpMfaHWzZuwhhBTcw=="], 89 | 90 | "@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-beta.50", "", { "os": "linux", "cpu": "x64" }, "sha512-gyoI8o/TGpQd3OzkJnh1M2kxy1Bisg8qJ5Gci0sXm9yLFzEXIFdtc4EAzepxGvrT2ri99ar5rdsmNG0zP0SbIg=="], 91 | 92 | "@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-beta.50", "", { "os": "linux", "cpu": "x64" }, "sha512-zti8A7M+xFDpKlghpcCAzyOi+e5nfUl3QhU023ce5NCgUxRG5zGP2GR9LTydQ1rnIPwZUVBWd4o7NjZDaQxaXA=="], 93 | 94 | "@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-beta.50", "", { "os": "none", "cpu": "arm64" }, "sha512-eZUssog7qljrrRU9Mi0eqYEPm3Ch0UwB+qlWPMKSUXHNqhm3TvDZarJQdTevGEfu3EHAXJvBIe0YFYr0TPVaMA=="], 95 | 96 | "@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-beta.50", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.0.7" }, "cpu": "none" }, "sha512-nmCN0nIdeUnmgeDXiQ+2HU6FT162o+rxnF7WMkBm4M5Ds8qTU7Dzv2Wrf22bo4ftnlrb2hKK6FSwAJSAe2FWLg=="], 97 | 98 | "@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-beta.50", "", { "os": "win32", "cpu": "arm64" }, "sha512-7kcNLi7Ua59JTTLvbe1dYb028QEPaJPJQHqkmSZ5q3tJueUeb6yjRtx8mw4uIqgWZcnQHAR3PrLN4XRJxvgIkA=="], 99 | 100 | "@rolldown/binding-win32-ia32-msvc": ["@rolldown/binding-win32-ia32-msvc@1.0.0-beta.50", "", { "os": "win32", "cpu": "ia32" }, "sha512-lL70VTNvSCdSZkDPPVMwWn/M2yQiYvSoXw9hTLgdIWdUfC3g72UaruezusR6ceRuwHCY1Ayu2LtKqXkBO5LIwg=="], 101 | 102 | "@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-beta.50", "", { "os": "win32", "cpu": "x64" }, "sha512-4qU4x5DXWB4JPjyTne/wBNPqkbQU8J45bl21geERBKtEittleonioACBL1R0PsBu0Aq21SwMK5a9zdBkWSlQtQ=="], 103 | 104 | "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.50", "", {}, "sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA=="], 105 | 106 | "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], 107 | 108 | "@types/bun": ["@types/bun@1.3.2", "", { "dependencies": { "bun-types": "1.3.2" } }, "sha512-t15P7k5UIgHKkxwnMNkJbWlh/617rkDGEdSsDbu+qNHTaz9SKf7aC8fiIlUdD5RPpH6GEkP0cK7WlvmrEBRtWg=="], 109 | 110 | "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], 111 | 112 | "@types/react": ["@types/react@19.2.2", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA=="], 113 | 114 | "@typescript/native-preview": ["@typescript/native-preview@7.0.0-dev.20251119.1", "", { "optionalDependencies": { "@typescript/native-preview-darwin-arm64": "7.0.0-dev.20251119.1", "@typescript/native-preview-darwin-x64": "7.0.0-dev.20251119.1", "@typescript/native-preview-linux-arm": "7.0.0-dev.20251119.1", "@typescript/native-preview-linux-arm64": "7.0.0-dev.20251119.1", "@typescript/native-preview-linux-x64": "7.0.0-dev.20251119.1", "@typescript/native-preview-win32-arm64": "7.0.0-dev.20251119.1", "@typescript/native-preview-win32-x64": "7.0.0-dev.20251119.1" }, "bin": { "tsgo": "bin/tsgo.js" } }, "sha512-oR7dJSdg1Ww2e/e7PCZd2ympNOY5ewTmcurQMuxsOz++Bacp12GZcPHnhPKtGcyHofIdcvbKTmtWe46iKIbTlA=="], 115 | 116 | "@typescript/native-preview-darwin-arm64": ["@typescript/native-preview-darwin-arm64@7.0.0-dev.20251119.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0SN4RzwpRAEKTmP1DARl/VueqXEw2NW5P+tr6Quxzly9nDhBm0vM2Zkf5xD/j9BmfCegUVUUiwazpmat5N7/Xg=="], 117 | 118 | "@typescript/native-preview-darwin-x64": ["@typescript/native-preview-darwin-x64@7.0.0-dev.20251119.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-NPb36JMIho0c8DfW8L4qw8R9x/RdylOA27RXr24WtGFSL/1gM6o1gVL0nX+imVeK9PC9dM7M7DM2YNmWN0V8kw=="], 119 | 120 | "@typescript/native-preview-linux-arm": ["@typescript/native-preview-linux-arm@7.0.0-dev.20251119.1", "", { "os": "linux", "cpu": "arm" }, "sha512-EZY9Qs82/s4o8baxm+xiw395Hl2RZaETwF3fdLCwrd27noFtqqnQE4lN///YZMNrTcN7PiB70wV6GoozwI44jQ=="], 121 | 122 | "@typescript/native-preview-linux-arm64": ["@typescript/native-preview-linux-arm64@7.0.0-dev.20251119.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-3e9Z2B9N/3TI1sLE+i9U8SpxNwngnhkmvYO683wdCaaIqeQXi1qBSb4UbOx5AKvtoYtUAR2sEn+1z6rtq6VJ3A=="], 123 | 124 | "@typescript/native-preview-linux-x64": ["@typescript/native-preview-linux-x64@7.0.0-dev.20251119.1", "", { "os": "linux", "cpu": "x64" }, "sha512-DnYuFQGYWJIorPqCnIu4A4OsJflkzNxjIOhe/2tYRRiCbA/GckxFJkFQ99KwcSDjA0UFWNQP4IbwnwNYawgfig=="], 125 | 126 | "@typescript/native-preview-win32-arm64": ["@typescript/native-preview-win32-arm64@7.0.0-dev.20251119.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-qgTIo9wkQee2Ai7H8dlVTPnQy4Q82JeCoGPqy+DtigE6IWfMheDbCW8xOrs3H8Ay6FYfo46/xlrhZtShmf7H1g=="], 127 | 128 | "@typescript/native-preview-win32-x64": ["@typescript/native-preview-win32-x64@7.0.0-dev.20251119.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lAQDi46tW0mGm3vEEo7VSMX/KKW+/VmRX8O/u58rdKYhLnZNZE+3+bgppN7S75RpTpttuv/hYC2scLFsOxk+hQ=="], 129 | 130 | "ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="], 131 | 132 | "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], 133 | 134 | "ansis": ["ansis@4.2.0", "", {}, "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig=="], 135 | 136 | "args-tokenizer": ["args-tokenizer@0.3.0", "", {}, "sha512-xXAd7G2Mll5W8uo37GETpQ2VrE84M181Z7ugHFGQnJZ50M2mbOv0osSZ9VsSgPfJQ+LVG0prSi0th+ELMsno7Q=="], 137 | 138 | "args-tokens": ["args-tokens@0.23.0", "", {}, "sha512-VZETsQmpEGO7A9adjfIOTyZuxKzurUas9kh1Hm0WGhFq8dOyw50P6OjiTwKyURwnmDabw0DbFjI+BUucJfCtnQ=="], 139 | 140 | "ast-kit": ["ast-kit@2.2.0", "", { "dependencies": { "@babel/parser": "^7.28.5", "pathe": "^2.0.3" } }, "sha512-m1Q/RaVOnTp9JxPX+F+Zn7IcLYMzM8kZofDImfsKZd8MbR+ikdOzTeztStWqfrqIxZnYWryyI9ePm3NGjnZgGw=="], 141 | 142 | "atomically": ["atomically@2.0.3", "", { "dependencies": { "stubborn-fs": "^1.2.5", "when-exit": "^2.1.1" } }, "sha512-kU6FmrwZ3Lx7/7y3hPS5QnbJfaohcIul5fGqf7ok+4KklIEk9tJ0C2IQPdacSbVUWv6zVHXEBWoWd6NrVMT7Cw=="], 143 | 144 | "birpc": ["birpc@2.8.0", "", {}, "sha512-Bz2a4qD/5GRhiHSwj30c/8kC8QGj12nNDwz3D4ErQ4Xhy35dsSDvF+RA/tWpjyU0pdGtSDiEk6B5fBGE1qNVhw=="], 145 | 146 | "bumpp": ["bumpp@10.3.1", "", { "dependencies": { "ansis": "^4.2.0", "args-tokenizer": "^0.3.0", "c12": "^3.3.0", "cac": "^6.7.14", "escalade": "^3.2.0", "jsonc-parser": "^3.3.1", "package-manager-detector": "^1.3.0", "semver": "^7.7.2", "tinyexec": "^1.0.1", "tinyglobby": "^0.2.15", "yaml": "^2.8.1" }, "bin": { "bumpp": "bin/bumpp.mjs" } }, "sha512-cOKPRFCWvHcYPJQAHN6V7Jp/wAfnyqQRXQ+2fgWIL6Gao20rpu7xQ1cGGo1APOfmbQmmHngEPg9Fy7nJ3giRkQ=="], 147 | 148 | "bun-types": ["bun-types@1.3.2", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-i/Gln4tbzKNuxP70OWhJRZz1MRfvqExowP7U6JKoI8cntFrtxg7RJK3jvz7wQW54UuvNC8tbKHHri5fy74FVqg=="], 149 | 150 | "c12": ["c12@3.3.1", "", { "dependencies": { "chokidar": "^4.0.3", "confbox": "^0.2.2", "defu": "^6.1.4", "dotenv": "^17.2.3", "exsolve": "^1.0.7", "giget": "^2.0.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.0.0", "pkg-types": "^2.3.0", "rc9": "^2.1.2" }, "peerDependencies": { "magicast": "^0.3.5" }, "optionalPeers": ["magicast"] }, "sha512-LcWQ01LT9tkoUINHgpIOv3mMs+Abv7oVCrtpMRi1PaapVEpWoMga5WuT7/DqFTu7URP9ftbOmimNw1KNIGh9DQ=="], 151 | 152 | "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], 153 | 154 | "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], 155 | 156 | "citty": ["citty@0.1.6", "", { "dependencies": { "consola": "^3.2.3" } }, "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ=="], 157 | 158 | "clipboardy": ["clipboardy@4.0.0", "", { "dependencies": { "execa": "^8.0.1", "is-wsl": "^3.1.0", "is64bit": "^2.0.0" } }, "sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w=="], 159 | 160 | "conf": ["conf@15.0.2", "", { "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "atomically": "^2.0.3", "debounce-fn": "^6.0.0", "dot-prop": "^10.0.0", "env-paths": "^3.0.0", "json-schema-typed": "^8.0.1", "semver": "^7.7.2", "uint8array-extras": "^1.5.0" } }, "sha512-JBSrutapCafTrddF9dH3lc7+T2tBycGF4uPkI4Js+g4vLLEhG6RZcFi3aJd5zntdf5tQxAejJt8dihkoQ/eSJw=="], 161 | 162 | "confbox": ["confbox@0.2.2", "", {}, "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ=="], 163 | 164 | "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], 165 | 166 | "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], 167 | 168 | "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], 169 | 170 | "debounce-fn": ["debounce-fn@6.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ=="], 171 | 172 | "defu": ["defu@6.1.4", "", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="], 173 | 174 | "destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="], 175 | 176 | "diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="], 177 | 178 | "dot-prop": ["dot-prop@10.1.0", "", { "dependencies": { "type-fest": "^5.0.0" } }, "sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q=="], 179 | 180 | "dotenv": ["dotenv@17.2.3", "", {}, "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w=="], 181 | 182 | "dts-resolver": ["dts-resolver@2.1.3", "", { "peerDependencies": { "oxc-resolver": ">=11.0.0" }, "optionalPeers": ["oxc-resolver"] }, "sha512-bihc7jPC90VrosXNzK0LTE2cuLP6jr0Ro8jk+kMugHReJVLIpHz/xadeq3MhuwyO4TD4OA3L1Q8pBBFRc08Tsw=="], 183 | 184 | "empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="], 185 | 186 | "env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], 187 | 188 | "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], 189 | 190 | "execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="], 191 | 192 | "exsolve": ["exsolve@1.0.7", "", {}, "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw=="], 193 | 194 | "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], 195 | 196 | "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], 197 | 198 | "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], 199 | 200 | "get-stream": ["get-stream@8.0.1", "", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="], 201 | 202 | "get-tsconfig": ["get-tsconfig@4.13.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ=="], 203 | 204 | "giget": ["giget@2.0.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "defu": "^6.1.4", "node-fetch-native": "^1.6.6", "nypm": "^0.6.0", "pathe": "^2.0.3" }, "bin": { "giget": "dist/cli.mjs" } }, "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA=="], 205 | 206 | "gunshi": ["gunshi@0.27.0-beta.3", "", { "dependencies": { "args-tokens": "^0.23.0" } }, "sha512-9WaE803OQlLjJUVMNOj0qohTzIdvo0y70fN2I4tWzNLBqpg8dM48ikEzhmULEZwH8EfQ6FpWfbppGZ7exUNmPg=="], 207 | 208 | "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], 209 | 210 | "human-signals": ["human-signals@5.0.0", "", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="], 211 | 212 | "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], 213 | 214 | "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], 215 | 216 | "is-stream": ["is-stream@3.0.0", "", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="], 217 | 218 | "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], 219 | 220 | "is64bit": ["is64bit@2.0.0", "", { "dependencies": { "system-architecture": "^0.1.0" } }, "sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw=="], 221 | 222 | "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], 223 | 224 | "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], 225 | 226 | "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], 227 | 228 | "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], 229 | 230 | "json-schema-typed": ["json-schema-typed@8.0.1", "", {}, "sha512-XQmWYj2Sm4kn4WeTYvmpKEbyPsL7nBsb647c7pMe6l02/yx2+Jfc4dT6UZkEXnIUb5LhD55r2HPsJ1milQ4rDg=="], 231 | 232 | "jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], 233 | 234 | "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], 235 | 236 | "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], 237 | 238 | "mimic-fn": ["mimic-fn@4.0.0", "", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="], 239 | 240 | "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], 241 | 242 | "node-fetch-native": ["node-fetch-native@1.6.6", "", {}, "sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ=="], 243 | 244 | "npm-run-path": ["npm-run-path@5.3.0", "", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="], 245 | 246 | "nypm": ["nypm@0.6.0", "", { "dependencies": { "citty": "^0.1.6", "consola": "^3.4.0", "pathe": "^2.0.3", "pkg-types": "^2.0.0", "tinyexec": "^0.3.2" }, "bin": { "nypm": "dist/cli.mjs" } }, "sha512-mn8wBFV9G9+UFHIrq+pZ2r2zL4aPau/by3kJb3cM7+5tQHMt6HGQB8FDIeKFYp8o0D2pnH6nVsO88N4AmUxIWg=="], 247 | 248 | "obug": ["obug@2.1.0", "", {}, "sha512-uu/tgLPoa75CFA7UDkmqspKbefvZh1WMPwkU3bNr0PY746a/+xwXVgbw5co5C3GvJj3h5u8g/pbxXzI0gd1QFg=="], 249 | 250 | "ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="], 251 | 252 | "onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="], 253 | 254 | "package-manager-detector": ["package-manager-detector@1.3.0", "", {}, "sha512-ZsEbbZORsyHuO00lY1kV3/t72yp6Ysay6Pd17ZAlNGuGwmWDLCJxFpRs0IzfXfj1o4icJOkUEioexFHzyPurSQ=="], 255 | 256 | "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], 257 | 258 | "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], 259 | 260 | "perfect-debounce": ["perfect-debounce@2.0.0", "", {}, "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow=="], 261 | 262 | "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], 263 | 264 | "pkg-types": ["pkg-types@2.3.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig=="], 265 | 266 | "quansync": ["quansync@0.2.11", "", {}, "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA=="], 267 | 268 | "rc9": ["rc9@2.1.2", "", { "dependencies": { "defu": "^6.1.4", "destr": "^2.0.3" } }, "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg=="], 269 | 270 | "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], 271 | 272 | "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], 273 | 274 | "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], 275 | 276 | "rolldown": ["rolldown@1.0.0-beta.50", "", { "dependencies": { "@oxc-project/types": "=0.97.0", "@rolldown/pluginutils": "1.0.0-beta.50" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-beta.50", "@rolldown/binding-darwin-arm64": "1.0.0-beta.50", "@rolldown/binding-darwin-x64": "1.0.0-beta.50", "@rolldown/binding-freebsd-x64": "1.0.0-beta.50", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.50", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.50", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.50", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.50", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.50", "@rolldown/binding-openharmony-arm64": "1.0.0-beta.50", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.50", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.50", "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.50", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.50" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-JFULvCNl/anKn99eKjOSEubi0lLmNqQDAjyEMME2T4CwezUDL0i6t1O9xZsu2OMehPnV2caNefWpGF+8TnzB6A=="], 277 | 278 | "rolldown-plugin-dts": ["rolldown-plugin-dts@0.17.8", "", { "dependencies": { "@babel/generator": "^7.28.5", "@babel/parser": "^7.28.5", "@babel/types": "^7.28.5", "ast-kit": "^2.2.0", "birpc": "^2.8.0", "dts-resolver": "^2.1.3", "get-tsconfig": "^4.13.0", "magic-string": "^0.30.21", "obug": "^2.0.0" }, "peerDependencies": { "@ts-macro/tsc": "^0.3.6", "@typescript/native-preview": ">=7.0.0-dev.20250601.1", "rolldown": "^1.0.0-beta.44", "typescript": "^5.0.0", "vue-tsc": "~3.1.0" }, "optionalPeers": ["@ts-macro/tsc", "@typescript/native-preview", "typescript", "vue-tsc"] }, "sha512-76EEBlhF00yeY6M7VpMkWKI4r9WjuoMiOGey7j4D6zf3m0BR+ZrrY9hvSXdueJ3ljxSLq4DJBKFpX/X9+L7EKw=="], 279 | 280 | "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], 281 | 282 | "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], 283 | 284 | "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], 285 | 286 | "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], 287 | 288 | "strip-final-newline": ["strip-final-newline@3.0.0", "", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="], 289 | 290 | "stubborn-fs": ["stubborn-fs@1.2.5", "", {}, "sha512-H2N9c26eXjzL/S/K+i/RHHcFanE74dptvvjM8iwzwbVcWY/zjBbgRqF3K0DY4+OD+uTTASTBvDoxPDaPN02D7g=="], 291 | 292 | "system-architecture": ["system-architecture@0.1.0", "", {}, "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA=="], 293 | 294 | "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], 295 | 296 | "tinyexec": ["tinyexec@1.0.1", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="], 297 | 298 | "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], 299 | 300 | "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], 301 | 302 | "tsdown": ["tsdown@0.16.5", "", { "dependencies": { "ansis": "^4.2.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "diff": "^8.0.2", "empathic": "^2.0.0", "hookable": "^5.5.3", "obug": "^2.0.0", "rolldown": "1.0.0-beta.50", "rolldown-plugin-dts": "^0.17.7", "semver": "^7.7.3", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tree-kill": "^1.2.2", "unconfig-core": "^7.4.1", "unrun": "^0.2.10" }, "peerDependencies": { "@arethetypeswrong/core": "^0.18.1", "@vitejs/devtools": "^0.0.0-alpha.17", "publint": "^0.3.0", "typescript": "^5.0.0", "unplugin-lightningcss": "^0.4.0", "unplugin-unused": "^0.5.0" }, "optionalPeers": ["@arethetypeswrong/core", "@vitejs/devtools", "publint", "typescript", "unplugin-lightningcss", "unplugin-unused"], "bin": { "tsdown": "dist/run.mjs" } }, "sha512-jo/2MmJI1uNJ+QvwEfF/2DcICd2Bc/Gc/XIVJS9Gvfns7ji5TgUeu3kYfG8nA/mGgWXU8REpTNweIcVJQoSLAQ=="], 303 | 304 | "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 305 | 306 | "type-fest": ["type-fest@5.1.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-wQ531tuWvB6oK+pchHIu5lHe5f5wpSCqB8Kf4dWQRbOYc9HTge7JL0G4Qd44bh6QuJCccIzL3bugb8GI0MwHrg=="], 307 | 308 | "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], 309 | 310 | "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], 311 | 312 | "unconfig-core": ["unconfig-core@7.4.1", "", { "dependencies": { "@quansync/fs": "^0.1.5", "quansync": "^0.2.11" } }, "sha512-Bp/bPZjV2Vl/fofoA2OYLSnw1Z0MOhCX7zHnVCYrazpfZvseBbGhwcNQMxsg185Mqh7VZQqK3C8hFG/Dyng+yA=="], 313 | 314 | "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], 315 | 316 | "unrun": ["unrun@0.2.11", "", { "dependencies": { "@oxc-project/runtime": "^0.96.0", "rolldown": "1.0.0-beta.51" }, "peerDependencies": { "synckit": "^0.11.11" }, "optionalPeers": ["synckit"], "bin": { "unrun": "dist/cli.mjs" } }, "sha512-HjUuNLRGfRxMvxkwOuO/CpkSzdizTPPApbarLplsTzUm8Kex+nS9eomKU1qgVus6WGWkDYhtf/mgNxGEpyTR6A=="], 317 | 318 | "when-exit": ["when-exit@2.1.4", "", {}, "sha512-4rnvd3A1t16PWzrBUcSDZqcAmsUIy4minDXT/CZ8F2mVDgd65i4Aalimgz1aQkRGU0iH5eT5+6Rx2TK8o443Pg=="], 319 | 320 | "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], 321 | 322 | "yaml": ["yaml@2.8.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw=="], 323 | 324 | "conf/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], 325 | 326 | "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], 327 | 328 | "nypm/pkg-types": ["pkg-types@2.2.0", "", { "dependencies": { "confbox": "^0.2.2", "exsolve": "^1.0.7", "pathe": "^2.0.3" } }, "sha512-2SM/GZGAEkPp3KWORxQZns4M+WSeXbC2HEvmOIJe3Cmiv6ieAJvdVhDldtHqM5J1Y7MrR1XhkBT/rMlhh9FdqQ=="], 329 | 330 | "nypm/tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], 331 | 332 | "tsdown/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], 333 | 334 | "tsdown/tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], 335 | 336 | "unrun/rolldown": ["rolldown@1.0.0-beta.51", "", { "dependencies": { "@oxc-project/types": "=0.98.0", "@rolldown/pluginutils": "1.0.0-beta.51" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-beta.51", "@rolldown/binding-darwin-arm64": "1.0.0-beta.51", "@rolldown/binding-darwin-x64": "1.0.0-beta.51", "@rolldown/binding-freebsd-x64": "1.0.0-beta.51", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-beta.51", "@rolldown/binding-linux-arm64-gnu": "1.0.0-beta.51", "@rolldown/binding-linux-arm64-musl": "1.0.0-beta.51", "@rolldown/binding-linux-x64-gnu": "1.0.0-beta.51", "@rolldown/binding-linux-x64-musl": "1.0.0-beta.51", "@rolldown/binding-openharmony-arm64": "1.0.0-beta.51", "@rolldown/binding-wasm32-wasi": "1.0.0-beta.51", "@rolldown/binding-win32-arm64-msvc": "1.0.0-beta.51", "@rolldown/binding-win32-ia32-msvc": "1.0.0-beta.51", "@rolldown/binding-win32-x64-msvc": "1.0.0-beta.51" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-ZRLgPlS91l4JztLYEZnmMcd3Umcla1hkXJgiEiR4HloRJBBoeaX8qogTu5Jfu36rRMVLndzqYv0h+M5gJAkUfg=="], 337 | 338 | "unrun/rolldown/@oxc-project/types": ["@oxc-project/types@0.98.0", "", {}, "sha512-Vzmd6FsqVuz5HQVcRC/hrx7Ujo3WEVeQP7C2UNP5uy1hUY4SQvMB+93jxkI1KRHz9a/6cni3glPOtvteN+zpsw=="], 339 | 340 | "unrun/rolldown/@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-beta.51", "", { "os": "android", "cpu": "arm64" }, "sha512-Ctn8FUXKWWQI9pWC61P1yumS9WjQtelNS9riHwV7oCkknPGaAry4o7eFx2KgoLMnI2BgFJYpW7Im8/zX3BuONg=="], 341 | 342 | "unrun/rolldown/@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-beta.51", "", { "os": "darwin", "cpu": "arm64" }, "sha512-EL1aRW2Oq15ShUEkBPsDtLMO8GTqfb/ktM/dFaVzXKQiEE96Ss6nexMgfgQrg8dGnNpndFyffVDb5IdSibsu1g=="], 343 | 344 | "unrun/rolldown/@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-beta.51", "", { "os": "darwin", "cpu": "x64" }, "sha512-uGtYKlFen9pMIPvkHPWZVDtmYhMQi5g5Ddsndg1gf3atScKYKYgs5aDP4DhHeTwGXQglhfBG7lEaOIZ4UAIWww=="], 345 | 346 | "unrun/rolldown/@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-beta.51", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JRoVTQtHYbZj1P07JLiuTuXjiBtIa7ag7/qgKA6CIIXnAcdl4LrOf7nfDuHPJcuRKaP5dzecMgY99itvWfmUFQ=="], 347 | 348 | "unrun/rolldown/@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-beta.51", "", { "os": "linux", "cpu": "arm" }, "sha512-BKATVnpPZ0TYBW9XfDwyd4kPGgvf964HiotIwUgpMrFOFYWqpZ+9ONNzMV4UFAYC7Hb5C2qgYQk/qj2OnAd4RQ=="], 349 | 350 | "unrun/rolldown/@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-beta.51", "", { "os": "linux", "cpu": "arm64" }, "sha512-xLd7da5jkfbVsBCm1buIRdWtuXY8+hU3+6ESXY/Tk5X5DPHaifrUblhYDgmA34dQt6WyNC2kfXGgrduPEvDI6Q=="], 351 | 352 | "unrun/rolldown/@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-beta.51", "", { "os": "linux", "cpu": "arm64" }, "sha512-EQFXTgHxxTzv3t5EmjUP/DfxzFYx9sMndfLsYaAY4DWF6KsK1fXGYsiupif6qPTViPC9eVmRm78q0pZU/kuIPg=="], 353 | 354 | "unrun/rolldown/@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-beta.51", "", { "os": "linux", "cpu": "x64" }, "sha512-p5P6Xpa68w3yFaAdSzIZJbj+AfuDnMDqNSeglBXM7UlJT14Q4zwK+rV+8Mhp9MiUb4XFISZtbI/seBprhkQbiQ=="], 355 | 356 | "unrun/rolldown/@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-beta.51", "", { "os": "linux", "cpu": "x64" }, "sha512-sNVVyLa8HB8wkFipdfz1s6i0YWinwpbMWk5hO5S+XAYH2UH67YzUT13gs6wZTKg2x/3gtgXzYnHyF5wMIqoDAw=="], 357 | 358 | "unrun/rolldown/@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-beta.51", "", { "os": "none", "cpu": "arm64" }, "sha512-e/JMTz9Q8+T3g/deEi8DK44sFWZWGKr9AOCW5e8C8SCVWzAXqYXAG7FXBWBNzWEZK0Rcwo9TQHTQ9Q0gXgdCaA=="], 359 | 360 | "unrun/rolldown/@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-beta.51", "", { "dependencies": { "@napi-rs/wasm-runtime": "^1.0.7" }, "cpu": "none" }, "sha512-We3LWqSu6J9s5Y0MK+N7fUiiu37aBGPG3Pc347EoaROuAwkCS2u9xJ5dpIyLW4B49CIbS3KaPmn4kTgPb3EyPw=="], 361 | 362 | "unrun/rolldown/@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-beta.51", "", { "os": "win32", "cpu": "arm64" }, "sha512-fj56buHRuMM+r/cb6ZYfNjNvO/0xeFybI6cTkTROJatdP4fvmQ1NS8D/Lm10FCSDEOkqIz8hK3TGpbAThbPHsA=="], 363 | 364 | "unrun/rolldown/@rolldown/binding-win32-ia32-msvc": ["@rolldown/binding-win32-ia32-msvc@1.0.0-beta.51", "", { "os": "win32", "cpu": "ia32" }, "sha512-fkqEqaeEx8AySXiDm54b/RdINb3C0VovzJA3osMhZsbn6FoD73H0AOIiaVAtGr6x63hefruVKTX8irAm4Jkt2w=="], 365 | 366 | "unrun/rolldown/@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-beta.51", "", { "os": "win32", "cpu": "x64" }, "sha512-CWuLG/HMtrVcjKGa0C4GnuxONrku89g0+CsH8nT0SNhOtREXuzwgjIXNJImpE/A/DMf9JF+1Xkrq/YRr+F/rCg=="], 367 | 368 | "unrun/rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.51", "", {}, "sha512-51/8cNXMrqWqX3o8DZidhwz1uYq0BhHDDSfVygAND1Skx5s1TDw3APSSxCMcFFedwgqGcx34gRouwY+m404BBQ=="], 369 | } 370 | } 371 | --------------------------------------------------------------------------------