diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4577459 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,62 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - name: Vet + run: go vet ./... + + - name: Test + run: go test ./... + + - name: Format check + run: | + unformatted=$(gofmt -l .) + if [ -n "$unformatted" ]; then + echo "Files need gofmt:" + echo "$unformatted" + exit 1 + fi + + build-snapshot: + name: Build snapshot + runs-on: ubuntu-latest + needs: test + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - uses: goreleaser/goreleaser-action@v6 + with: + version: latest + args: release --snapshot --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: dist-snapshot + path: dist/ + retention-days: 7 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..d6d0909 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,53 @@ +name: Release + +on: + push: + tags: + - 'v*' + +jobs: + release: + name: Release + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-go@v5 + with: + go-version-file: go.mod + cache: true + + - uses: goreleaser/goreleaser-action@v6 + with: + version: latest + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + vscode-package: + name: VSCode Extension + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: npm + cache-dependency-path: editors/vscode/package-lock.json + + - name: Install and package + working-directory: editors/vscode + run: | + npm ci + npm run compile + npm run package + + - uses: actions/upload-artifact@v4 + with: + name: pgtidy-vscode + path: editors/vscode/*.vsix diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..bba790f --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,54 @@ +project_name: pgtidy + +before: + hooks: + - go mod tidy + - go vet ./... + +builds: + - id: pgtidy + main: ./cmd/pgtidy + binary: pgtidy + ldflags: + - -s -w -X main.version={{ .Version }} + env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + - windows + goarch: + - amd64 + - arm64 + ignore: + - goos: windows + goarch: arm64 + +archives: + - id: pgtidy + name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" + format_overrides: + - goos: windows + formats: [zip] + files: + - LICENSE + - README.md + +checksum: + name_template: "checksums.txt" + +changelog: + sort: asc + use: github + filters: + exclude: + - "^docs:" + - "^test:" + - "^chore:" + - "^ci:" + +release: + github: + owner: hein + name: pgtidy + draft: true diff --git a/Makefile b/Makefile index 98eb0ae..8ebfd9c 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,7 @@ APP := pgtidy CMD := ./cmd/pgtidy DIST := dist -.PHONY: build test lint vet fmt clean +.PHONY: build test lint vet fmt clean release snapshot vscode-compile vscode-package ## build: compile binary for the current platform build: @@ -27,3 +27,20 @@ lint: vet ## clean: remove build artifacts clean: rm -rf $(DIST) + +## snapshot: build multi-platform binaries without publishing (requires goreleaser) +snapshot: + goreleaser release --snapshot --clean + +## release: tag and publish a GitHub release (requires GITHUB_TOKEN + goreleaser) +release: + goreleaser release --clean + +## vscode-compile: compile the VSCode extension TypeScript +vscode-compile: + cp -r assets editors/vscode/assets + cd editors/vscode && pnpm install && pnpm run build + +## vscode-package: build the .vsix package +vscode-package: vscode-compile + cd editors/vscode && pnpm run package diff --git a/assets/logo_1024.png b/assets/logo_1024.png new file mode 100644 index 0000000..4c05cbb Binary files /dev/null and b/assets/logo_1024.png differ diff --git a/assets/logo_128.ico b/assets/logo_128.ico new file mode 100644 index 0000000..a92f130 Binary files /dev/null and b/assets/logo_128.ico differ diff --git a/assets/logo_128.png b/assets/logo_128.png new file mode 100644 index 0000000..7331961 Binary files /dev/null and b/assets/logo_128.png differ diff --git a/assets/logo_256.png b/assets/logo_256.png new file mode 100644 index 0000000..031c505 Binary files /dev/null and b/assets/logo_256.png differ diff --git a/cmd/pgtidy/config.go b/cmd/pgtidy/config.go new file mode 100644 index 0000000..231ac9e --- /dev/null +++ b/cmd/pgtidy/config.go @@ -0,0 +1,34 @@ +package main + +import ( + "fmt" + "io" + "os" + + "git.warky.dev/wdevs/pgtidy/pkg/config" +) + +func cmdConfig(args []string, stdin io.Reader, stdout, stderr io.Writer) int { + for _, a := range args { + if a == "-h" || a == "--help" { + fmt.Fprintln(stdout, "pgtidy config — print effective configuration resolved from .pgtidy.yaml") + return 0 + } + } + wd, err := os.Getwd() + if err != nil { + wd = "." + } + st, err := config.Load(wd) + if err != nil { + fmt.Fprintf(stderr, "pgtidy: %v\n", err) + return 2 + } + fmt.Fprintf(stdout, "indent: %q\n", st.Indent) + fmt.Fprintf(stdout, "newline: %q\n", st.Newline) + fmt.Fprintf(stdout, "keyword_case: %s\n", st.KeywordCase) + fmt.Fprintf(stdout, "ident_case: %s\n", st.IdentCase) + fmt.Fprintf(stdout, "type_case: %s\n", st.TypeCase) + fmt.Fprintf(stdout, "commas: %s\n", st.Commas) + return 0 +} diff --git a/cmd/pgtidy/fmt.go b/cmd/pgtidy/fmt.go index c01d560..66c4389 100644 --- a/cmd/pgtidy/fmt.go +++ b/cmd/pgtidy/fmt.go @@ -5,9 +5,9 @@ import ( "io" "os" - "github.com/hein/pgtidy/pkg/config" - "github.com/hein/pgtidy/pkg/format" - "github.com/hein/pgtidy/pkg/parser" + "git.warky.dev/wdevs/pgtidy/pkg/config" + "git.warky.dev/wdevs/pgtidy/pkg/format" + "git.warky.dev/wdevs/pgtidy/pkg/parser" ) // cmdFmt implements `pgtidy fmt`. It follows the gofmt model: with no flags it diff --git a/cmd/pgtidy/lint.go b/cmd/pgtidy/lint.go index b9a78ef..c2c0aa3 100644 --- a/cmd/pgtidy/lint.go +++ b/cmd/pgtidy/lint.go @@ -6,8 +6,8 @@ import ( "os" "strings" - "github.com/hein/pgtidy/pkg/diagnostics" - "github.com/hein/pgtidy/pkg/lint" + "git.warky.dev/wdevs/pgtidy/pkg/diagnostics" + "git.warky.dev/wdevs/pgtidy/pkg/lint" ) // cmdLint implements `pgtidy lint`. Reads one or more SQL files (or stdin) and @@ -19,6 +19,7 @@ import ( func cmdLint(args []string, stdin io.Reader, stdout, stderr io.Writer) int { var ( only []string // --only=ID,ID rule filter + fix bool files []string ) for _, a := range args { @@ -26,6 +27,8 @@ func cmdLint(args []string, stdin io.Reader, stdout, stderr io.Writer) int { case a == "-h" || a == "--help": lintUsage(stdout) return 0 + case a == "--fix": + fix = true case strings.HasPrefix(a, "--only="): ids := strings.Split(strings.TrimPrefix(a, "--only="), ",") for _, id := range ids { @@ -89,6 +92,13 @@ func cmdLint(args []string, stdin io.Reader, stdout, stderr io.Writer) int { fmt.Fprintf(stderr, "pgtidy: %v\n", err) return 2 } + if fix { + fixed := lint.ApplyFixes(string(src), diags) + if fixed != string(src) { + io.WriteString(stdout, fixed) + return 0 + } + } printDiags(diags) if len(diags) > 0 { found = true @@ -105,6 +115,21 @@ func cmdLint(args []string, stdin io.Reader, stdout, stderr io.Writer) int { fmt.Fprintf(stderr, "pgtidy: %v\n", err) return 2 } + if fix { + fixed := lint.ApplyFixes(string(src), diags) + if fixed != string(src) { + if err := os.WriteFile(path, []byte(fixed), 0o644); err != nil { + fmt.Fprintf(stderr, "pgtidy: writing %s: %v\n", path, err) + return 2 + } + // Re-check to report any remaining unfixed diagnostics. + diags, err = check(fixed, path) + if err != nil { + fmt.Fprintf(stderr, "pgtidy: %v\n", err) + return 2 + } + } + } printDiags(diags) if len(diags) > 0 { found = true @@ -124,6 +149,7 @@ func lintUsage(w io.Writer) { Read SQL from files (or stdin) and report lint findings. Flags: + --fix Apply autofixes for fixable rules (MIG001, MIG003) and rewrite files --only=ID,... comma-separated rule IDs to enable (default: all rules) -h, --help show this help diff --git a/cmd/pgtidy/lsp.go b/cmd/pgtidy/lsp.go new file mode 100644 index 0000000..936c07f --- /dev/null +++ b/cmd/pgtidy/lsp.go @@ -0,0 +1,28 @@ +package main + +import ( + "context" + "fmt" + "io" + "os" + + "git.warky.dev/wdevs/pgtidy/pkg/lsp" +) + +func cmdLsp(args []string, stdin io.Reader, stdout, stderr io.Writer) int { + for _, a := range args { + if a == "-h" || a == "--help" { + fmt.Fprintln(stdout, "pgtidy lsp — start the Language Server Protocol server (stdio transport)") + return 0 + } + } + wd, err := os.Getwd() + if err != nil { + wd = "." + } + if err := lsp.Serve(context.Background(), stdin, stdout, wd); err != nil { + fmt.Fprintf(stderr, "pgtidy lsp: %v\n", err) + return 1 + } + return 0 +} diff --git a/cmd/pgtidy/main.go b/cmd/pgtidy/main.go index f29ad1d..04cdae5 100644 --- a/cmd/pgtidy/main.go +++ b/cmd/pgtidy/main.go @@ -24,6 +24,10 @@ func run(args []string, stdin io.Reader, stdout, stderr io.Writer) int { return cmdFmt(args[1:], stdin, stdout, stderr) case "lint": return cmdLint(args[1:], stdin, stdout, stderr) + case "lsp": + return cmdLsp(args[1:], stdin, stdout, stderr) + case "config": + return cmdConfig(args[1:], stdin, stdout, stderr) case "version", "--version", "-v": fmt.Fprintf(stdout, "pgtidy %s\n", version) return 0 @@ -41,10 +45,12 @@ func usage(w io.Writer) { fmt.Fprint(w, `pgtidy — PostgreSQL formatter and linter Usage: - pgtidy fmt [flags] [files...] Format SQL/PL-pgSQL (stdin if no files) - pgtidy lint [flags] [files...] Lint SQL (stdin if no files) - pgtidy version Print version - pgtidy help Show this help + pgtidy fmt [flags] [files...] Format SQL/PL-pgSQL (stdin if no files) + pgtidy lint [flags] [files...] Lint SQL (stdin if no files) + pgtidy config Print effective configuration + pgtidy lsp Start LSP server (stdio, for editors) + pgtidy version Print version + pgtidy help Show this help fmt flags: -w, --write Rewrite files in place diff --git a/docs/todo.md b/docs/todo.md index eb67906..7418576 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -9,7 +9,7 @@ Legend: ✅ done · 🚧 in progress · ⬜ not started ## V1 — Formatter + CLI (current milestone) ### ✅ Scaffold module + repo hygiene -- `go.mod` (`module github.com/hein/pgtidy`, go 1.26). +- `go.mod` (`module git.warky.dev/wdevs/pgtidy`, go 1.26). - Replaced WkMailSync boilerplate: `AGENTS.md` (PgTidy architecture + invariants), `CLAUDE.md`, `Makefile` (`build`/`test`/`vet`/`fmt`/`lint`/`clean`). - Directory layout created (`cmd/`, `pkg/...`, `testdata/`). @@ -108,21 +108,33 @@ Legend: ✅ done · 🚧 in progress · ⬜ not started - NAM001/2/3 table/column/function names not snake_case (quoted identifiers only) - `pgtidy lint [--only=ID,...] [files...]`; exits 1 on findings, 2 on error. - Fixture SQL in `testdata/lint/`; 6 tests covering violations + clean fixtures. -- _`--fix` for autofixable rules: future._ +- `--fix` rewrites files in place applying autofixes; for stdin, prints fixed SQL to stdout. +- Autofixable: **MIG001** (insert `CONCURRENTLY` after `INDEX`) and **MIG003** (insert `NOT VALID` before `;`). MIG002, COR*, NAM* are intentionally not autofixable. +- `pkg/diagnostics.TextFix{Offset, End, New, Title}` — byte-range replacement attached to `Diagnostic.Fix`. +- `pkg/lint.ApplyFixes` — applies all fixes in reverse-offset order; overlapping fixes skipped. +- Fix helpers (`mig001Fix`, `mig003Fix`) handle pg_query's convention of `StmtLen` excluding the trailing `;`. -## V3 — LSP + VSCode (later) -- `pkg/lsp`: formatting + range formatting, publishDiagnostics, codeAction quick-fixes. -- `editors/vscode`: TS extension (`vscode-languageclient`) launching bundled `pgtidy lsp`; - per-platform VSIX matrix in CI (rust-analyzer model) + target-less fallback. +## ✅ V3 — LSP + VSCode +- `pkg/lsp`: JSON-RPC 2.0 over stdio; `textDocument/formatting` (full document), `publishDiagnostics` on every open/change, `textDocument/codeAction` quick-fixes, lifecycle (initialize/shutdown/exit). No external deps. +- `cmd/pgtidy/lsp.go`: `pgtidy lsp` subcommand; config discovered from cwd. +- `editors/vscode/`: TS extension using `vscode-languageclient`; launches `pgtidy lsp` via stdio; `.pgsql` mapped to `sql` language; `pgtidy.path` / `pgtidy.enable` settings. +- _Range formatting: future._ -## V4 — DataGrip (later) -- `editors/datagrip`: integrate via free **LSP4IJ** plugin. +## ✅ V4 — DataGrip +- `editors/datagrip/`: Gradle-based JetBrains plugin targeting DataGrip 2024.3+ via LSP4IJ. + - `build.gradle.kts` / `settings.gradle.kts` / `gradle.properties` — IntelliJ Platform Gradle Plugin v2. + - `plugin.xml` — registers `PgTidyServerFactory` as an LSP4IJ `` extension and maps `*.sql`/`*.pgsql` to it. + - `PgTidyServerFactory.kt` + `PgTidyServerConnection.kt` — launches `pgtidy lsp` via `ProcessStreamConnectionProvider`. + - Requires LSP4IJ plugin installed in the IDE; `pgtidy` binary on PATH. --- -## Build / release (cross-cutting) -- ⬜ Add goreleaser for the multi-platform binary matrix (clean: no cgo). -- `make_release.sh` retained from boilerplate (generic version tagging). +## ✅ Build / release (cross-cutting) +- `.goreleaser.yaml`: multi-platform matrix — linux/darwin × amd64/arm64 + windows/amd64; no CGO; ldflags version injection; draft GitHub release. +- `Makefile` extended: `snapshot` (local multi-platform build), `release` (publish), `vscode-compile`, `vscode-package`. +- `.github/workflows/ci.yml`: test + vet + gofmt check + goreleaser snapshot on every push/PR. +- `.github/workflows/release.yml`: goreleaser publish + VSCode `.vsix` artifact on `v*` tag. +- `make_release.sh` retained from boilerplate. ## Core invariants (must always hold — tested) 1. ✅ Lossless lex: `emit(Lex(src)) == src` (corpus round-trip). diff --git a/editors/datagrip/.gitignore b/editors/datagrip/.gitignore new file mode 100644 index 0000000..5675024 --- /dev/null +++ b/editors/datagrip/.gitignore @@ -0,0 +1,3 @@ +.gradle/ +build/ +*.zip diff --git a/editors/datagrip/README.md b/editors/datagrip/README.md new file mode 100644 index 0000000..5aab621 --- /dev/null +++ b/editors/datagrip/README.md @@ -0,0 +1,37 @@ +# PgTidy — DataGrip / JetBrains Plugin + +Formats and lints SQL files in DataGrip (and any JetBrains IDE) via the LSP4IJ plugin. + +## Requirements + +- `pgtidy` binary on `PATH` — download from [releases](https://git.warky.dev/wdevs/PgTidy/releases) or build with `go install git.warky.dev/wdevs/pgtidy/cmd/pgtidy@latest` +- [LSP4IJ](https://plugins.jetbrains.com/plugin/23257-lsp4ij) plugin installed (free, by Red Hat) +- DataGrip 2024.3+ (or any JetBrains IDE 2024.3+) + +## Install the plugin + +### Option A — Install from disk (`.zip`) + +1. Build: `./gradlew buildPlugin` (output in `build/distributions/`) +2. In DataGrip: **Settings → Plugins → ⚙ → Install Plugin from Disk…** → select the `.zip` +3. Restart the IDE + +### Option B — Install LSP4IJ and configure manually (no plugin build needed) + +1. Install **LSP4IJ** from the marketplace (**Settings → Plugins → Marketplace → search "LSP4IJ"**) +2. Go to **Settings → Language Servers → + (Add)** +3. Fill in: + - **Name:** `PgTidy` + - **Command:** `pgtidy lsp` +4. Under **Mappings**, add file patterns: `*.sql`, `*.pgsql` +5. Click **OK** and restart the IDE + +## Features + +- **Formatting** — `Code → Reformat Code` (`Ctrl+Alt+L`) formats the current SQL file +- **Diagnostics** — lint findings shown as inspections (MIG001–MIG003, COR001–003, NAM001–003) +- **Quick fixes** — intention actions for MIG001 (add `CONCURRENTLY`) and MIG003 (add `NOT VALID`) + +## Troubleshooting + +**Server not starting:** Check **View → Tool Windows → LSP4IJ Consoles** for stderr output. Most common cause: `pgtidy` not found on `PATH` — update the command to the full path, e.g. `/usr/local/bin/pgtidy lsp`. diff --git a/editors/datagrip/build.gradle.kts b/editors/datagrip/build.gradle.kts new file mode 100644 index 0000000..474ca3a --- /dev/null +++ b/editors/datagrip/build.gradle.kts @@ -0,0 +1,48 @@ +plugins { + id("org.jetbrains.intellij.platform") version "2.3.0" + kotlin("jvm") version "2.0.0" +} + +group = providers.gradleProperty("pluginGroup").get() +version = providers.gradleProperty("pluginVersion").get() + +kotlin { + jvmToolchain(17) +} + +repositories { + mavenCentral() + intellijPlatform { + defaultRepositories() + } +} + +dependencies { + intellijPlatform { + val platformVersion = providers.gradleProperty("platformVersion") + val platformType = providers.gradleProperty("platformType") + create(platformType, platformVersion) + bundledPlugin("com.intellij.database") + plugin("com.redhat.devtools.lsp4ij:${providers.gradleProperty("lsp4ijVersion").get()}") + instrumentationTools() + } +} + +intellijPlatform { + pluginConfiguration { + name = providers.gradleProperty("pluginName") + version = providers.gradleProperty("pluginVersion") + ideaVersion { + sinceBuild = providers.gradleProperty("pluginSinceBuild") + } + } + signing { + // Set CERTIFICATE_CHAIN, PRIVATE_KEY, PRIVATE_KEY_PASSWORD env vars for release signing. + certificateChain = providers.environmentVariable("CERTIFICATE_CHAIN") + privateKey = providers.environmentVariable("PRIVATE_KEY") + privateKeyPassword = providers.environmentVariable("PRIVATE_KEY_PASSWORD") + } + publishing { + token = providers.environmentVariable("PUBLISH_TOKEN") + } +} diff --git a/editors/datagrip/gradle.properties b/editors/datagrip/gradle.properties new file mode 100644 index 0000000..6dfc5d5 --- /dev/null +++ b/editors/datagrip/gradle.properties @@ -0,0 +1,8 @@ +pluginGroup=com.pgtidy +pluginName=PgTidy +pluginVersion=0.1.0 +pluginSinceBuild=243 +# DataGrip 2024.3 +platformVersion=2024.3 +platformType=DB +lsp4ijVersion=0.8.0 diff --git a/editors/datagrip/settings.gradle.kts b/editors/datagrip/settings.gradle.kts new file mode 100644 index 0000000..4a2ccd3 --- /dev/null +++ b/editors/datagrip/settings.gradle.kts @@ -0,0 +1,5 @@ +rootProject.name = "pgtidy-datagrip" + +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" +} diff --git a/editors/datagrip/src/main/kotlin/com/pgtidy/datagrip/PgTidyServerConnection.kt b/editors/datagrip/src/main/kotlin/com/pgtidy/datagrip/PgTidyServerConnection.kt new file mode 100644 index 0000000..7cf955b --- /dev/null +++ b/editors/datagrip/src/main/kotlin/com/pgtidy/datagrip/PgTidyServerConnection.kt @@ -0,0 +1,9 @@ +package com.pgtidy.datagrip + +import com.intellij.openapi.project.Project +import com.redhat.devtools.lsp4ij.server.ProcessStreamConnectionProvider + +class PgTidyServerConnection(project: Project) : ProcessStreamConnectionProvider( + listOf("pgtidy", "lsp"), + project.basePath ?: System.getProperty("user.home"), +) diff --git a/editors/datagrip/src/main/kotlin/com/pgtidy/datagrip/PgTidyServerFactory.kt b/editors/datagrip/src/main/kotlin/com/pgtidy/datagrip/PgTidyServerFactory.kt new file mode 100644 index 0000000..5c1a202 --- /dev/null +++ b/editors/datagrip/src/main/kotlin/com/pgtidy/datagrip/PgTidyServerFactory.kt @@ -0,0 +1,10 @@ +package com.pgtidy.datagrip + +import com.intellij.openapi.project.Project +import com.redhat.devtools.lsp4ij.LanguageServerFactory +import com.redhat.devtools.lsp4ij.server.StreamConnectionProvider + +class PgTidyServerFactory : LanguageServerFactory { + override fun createConnectionProvider(project: Project): StreamConnectionProvider = + PgTidyServerConnection(project) +} diff --git a/editors/datagrip/src/main/resources/META-INF/plugin.xml b/editors/datagrip/src/main/resources/META-INF/plugin.xml new file mode 100644 index 0000000..f9a56fb --- /dev/null +++ b/editors/datagrip/src/main/resources/META-INF/plugin.xml @@ -0,0 +1,27 @@ + + com.pgtidy.datagrip + PgTidy + 0.1.0 + Warky Devs + pgtidy.
+ Requires the pgtidy binary on PATH and the + LSP4IJ plugin. + ]]>
+ + com.intellij.modules.platform + com.intellij.database + com.redhat.devtools.lsp4ij + + + + PostgreSQL formatter and linter (pgtidy lsp) + + + + +
diff --git a/editors/datagrip/src/main/resources/META-INF/pluginIcon.png b/editors/datagrip/src/main/resources/META-INF/pluginIcon.png new file mode 100644 index 0000000..031c505 Binary files /dev/null and b/editors/datagrip/src/main/resources/META-INF/pluginIcon.png differ diff --git a/editors/vscode/.gitignore b/editors/vscode/.gitignore new file mode 100644 index 0000000..81b9b88 --- /dev/null +++ b/editors/vscode/.gitignore @@ -0,0 +1,4 @@ +out/ +node_modules/ +*.vsix +assets/ diff --git a/editors/vscode/.vscodeignore b/editors/vscode/.vscodeignore new file mode 100644 index 0000000..7524234 --- /dev/null +++ b/editors/vscode/.vscodeignore @@ -0,0 +1,10 @@ +.vscode/** +src/** +node_modules/** +.gitignore +.gitattributes +tsconfig.json +pnpm-lock.yaml +pnpm-workspace.yaml +**/*.ts +**/*.map diff --git a/editors/vscode/LICENSE b/editors/vscode/LICENSE new file mode 120000 index 0000000..30cff74 --- /dev/null +++ b/editors/vscode/LICENSE @@ -0,0 +1 @@ +../../LICENSE \ No newline at end of file diff --git a/editors/vscode/README.md b/editors/vscode/README.md new file mode 100644 index 0000000..988e1b3 --- /dev/null +++ b/editors/vscode/README.md @@ -0,0 +1,75 @@ +# PgTidy — VSCode Extension + +Formats and lints PostgreSQL/PL-pgSQL files using the `pgtidy` language server. + +## Requirements + +- `pgtidy` binary on `PATH` (see [Build the binary](#build-the-binary) below) +- Go 1.21+ (to build from source) +- VSCode 1.85+ + +## Build the binary + +**Option A — install directly with Go (recommended):** + +```sh +go install git.warky.dev/wdevs/pgtidy/cmd/pgtidy@latest +``` + +The binary lands in `$(go env GOPATH)/bin/`. Make sure that directory is on your `PATH`. + +**Option B — build from source and place manually:** + +```sh +git clone https://git.warky.dev/wdevs/PgTidy +cd PgTidy +go build -o pgtidy ./cmd/pgtidy + +# Linux / macOS — move to a directory on PATH +mv pgtidy /usr/local/bin/ + +# Windows — move to a directory on PATH, e.g. +# move pgtidy.exe C:\tools\ +``` + +Verify the binary is reachable: + +```sh +pgtidy version +``` + +If you place it somewhere not on `PATH`, set the full path in VSCode settings: + +```json +"pgtidy.path": "/path/to/pgtidy" +``` + +## Install from `.vsix` + +1. Build the package: `pnpm run build && pnpm run package` +2. In VSCode: **Extensions** → `···` menu → **Install from VSIX…** → select `pgtidy-*.vsix` + +Or via CLI: + +```sh +code --install-extension pgtidy-0.1.0.vsix +``` + +## Settings + +| Setting | Default | Description | +|---|---|---| +| `pgtidy.path` | `"pgtidy"` | Path to the binary if not on `PATH` | +| `pgtidy.enable` | `true` | Enable/disable the language server | + +## Features + +- **Format on save** — configure via VSCode's `editor.formatOnSave` +- **Diagnostics** — lint findings shown inline as you type (MIG001–MIG003, COR001–003, NAM001–003) +- **Quick fixes** — lightbulb actions for MIG001 (add `CONCURRENTLY`) and MIG003 (add `NOT VALID`) + +`.sql` and `.pgsql` files are both handled. + +## Troubleshooting + +**No formatting / diagnostics:** Open the Output panel → select **PgTidy** to see server errors. Most common cause: `pgtidy` not found — set `pgtidy.path` to the full binary path. diff --git a/editors/vscode/package.json b/editors/vscode/package.json new file mode 100644 index 0000000..aa19824 --- /dev/null +++ b/editors/vscode/package.json @@ -0,0 +1,88 @@ +{ + "name": "pgtidy", + "displayName": "PgTidy", + "description": "PostgreSQL formatter and linter powered by pgtidy", + "version": "0.1.0", + "publisher": "warky-devs", + "author": { + "name": "Hein Puth", + "email": "hein.puth@gmail.com" + }, + "icon": "assets/logo_256.png", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://git.warky.dev/wdevs/PgTidy" + }, + "engines": { + "vscode": "^1.85.0" + }, + "categories": [ + "Formatters", + "Linters" + ], + "activationEvents": [ + "onCommand:pgtidy.showVersion", + "onCommand:pgtidy.showConfig", + "onCommand:pgtidy.formatDocument" + ], + "main": "./out/extension", + "contributes": { + "commands": [ + { + "command": "pgtidy.showVersion", + "title": "Show Version", + "category": "PgTidy" + }, + { + "command": "pgtidy.showConfig", + "title": "Show Config", + "category": "PgTidy" + }, + { + "command": "pgtidy.formatDocument", + "title": "Format Document", + "category": "PgTidy" + } + ], + "languages": [ + { + "id": "sql", + "extensions": [ + ".pgsql" + ] + } + ], + "configuration": { + "title": "PgTidy", + "properties": { + "pgtidy.path": { + "type": "string", + "default": "pgtidy", + "description": "Path to the pgtidy binary. Defaults to 'pgtidy' (must be on PATH)." + }, + "pgtidy.enable": { + "type": "boolean", + "default": true, + "description": "Enable the PgTidy language server." + } + } + } + }, + "scripts": { + "build": "tsc -p ./", + "compile": "tsc -p ./", + "watch": "tsc -watch -p ./", + "package": "vsce package --no-dependencies", + "lint": "eslint src --ext ts" + }, + "dependencies": { + "vscode-languageclient": "^9.0.1" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "@types/vscode": "^1.85.0", + "@vscode/vsce": "^2.24.0", + "typescript": "^5.3.0" + } +} \ No newline at end of file diff --git a/editors/vscode/pnpm-lock.yaml b/editors/vscode/pnpm-lock.yaml new file mode 100644 index 0000000..382dc10 --- /dev/null +++ b/editors/vscode/pnpm-lock.yaml @@ -0,0 +1,1589 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + vscode-languageclient: + specifier: ^9.0.1 + version: 9.0.1 + devDependencies: + '@types/node': + specifier: ^20.0.0 + version: 20.19.43 + '@types/vscode': + specifier: ^1.85.0 + version: 1.125.0 + '@vscode/vsce': + specifier: ^2.24.0 + version: 2.32.0 + typescript: + specifier: ^5.3.0 + version: 5.9.3 + +packages: + + '@azure/abort-controller@2.1.2': + resolution: {integrity: sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==} + engines: {node: '>=18.0.0'} + + '@azure/core-auth@1.10.1': + resolution: {integrity: sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==} + engines: {node: '>=20.0.0'} + + '@azure/core-client@1.10.2': + resolution: {integrity: sha512-1D2LpsU7y9xrqKjdIbsB7PlrRePw0xsVV8p+AKTlzITrWmscajryfJCdDJB/oGwvDI5HmRo04eMMADB67uwAwQ==} + engines: {node: '>=20.0.0'} + + '@azure/core-rest-pipeline@1.24.0': + resolution: {integrity: sha512-PpLsoDQ3AMmKZ0VU+0GrmqMxgp/sExjlVm4R+nLWngeoEGAzOIPVifaxKGU5gMv+nWELUoHfvrolWD+ZS/nFJg==} + engines: {node: '>=20.0.0'} + + '@azure/core-tracing@1.3.1': + resolution: {integrity: sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==} + engines: {node: '>=20.0.0'} + + '@azure/core-util@1.13.1': + resolution: {integrity: sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==} + engines: {node: '>=20.0.0'} + + '@azure/identity@4.13.1': + resolution: {integrity: sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw==} + engines: {node: '>=20.0.0'} + + '@azure/logger@1.3.0': + resolution: {integrity: sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==} + engines: {node: '>=20.0.0'} + + '@azure/msal-browser@5.15.0': + resolution: {integrity: sha512-2NYT6v+eeQn8kmNddr9LnbXSvXbVELpmFMmfFvtRxD7I/5+5GlkMlncApeuRFj+mY6C9syOwQip1a0Y+TIbyiA==} + engines: {node: '>=0.8.0'} + + '@azure/msal-common@16.10.0': + resolution: {integrity: sha512-iYtjpanlv6963Jprs0MvzIap07V+QhultjQctfbEDQCflsDAEeO3R7XnVA5gk30fhoBFLdgJT7VqO0TGsEsN9w==} + engines: {node: '>=0.8.0'} + + '@azure/msal-node@5.3.0': + resolution: {integrity: sha512-fXtJX811pX8y8QlrQqBSH6+plvWyKZDI0IxkheAcyAw9OtcpXyFivmTC7eGUqutLWaDlKXuQ3yOESD4zAmkjHg==} + engines: {node: '>=20'} + + '@types/node@20.19.43': + resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==} + + '@types/vscode@1.125.0': + resolution: {integrity: sha512-0icm/ZQAaism87P0ekHqi4/Ju9du+Tm0RUW+y7vqRsxY2cY0FNRX1nAnaW7nT6npPt2tfHiheZ55Zm9UhqonFA==} + + '@typespec/ts-http-runtime@0.3.6': + resolution: {integrity: sha512-jIXhD0eWQ1JA6ln/5Dltyx22UxWNrw0hZmhy2rlv6m6KgF7kplHx3g0fzi09lNmTJQRR91OlemYp3xFnvDK9og==} + engines: {node: '>=20.0.0'} + + '@vscode/vsce-sign-alpine-arm64@2.0.6': + resolution: {integrity: sha512-wKkJBsvKF+f0GfsUuGT0tSW0kZL87QggEiqNqK6/8hvqsXvpx8OsTEc3mnE1kejkh5r+qUyQ7PtF8jZYN0mo8Q==} + cpu: [arm64] + os: [alpine] + + '@vscode/vsce-sign-alpine-x64@2.0.6': + resolution: {integrity: sha512-YoAGlmdK39vKi9jA18i4ufBbd95OqGJxRvF3n6ZbCyziwy3O+JgOpIUPxv5tjeO6gQfx29qBivQ8ZZTUF2Ba0w==} + cpu: [x64] + os: [alpine] + + '@vscode/vsce-sign-darwin-arm64@2.0.6': + resolution: {integrity: sha512-5HMHaJRIQuozm/XQIiJiA0W9uhdblwwl2ZNDSSAeXGO9YhB9MH5C4KIHOmvyjUnKy4UCuiP43VKpIxW1VWP4tQ==} + cpu: [arm64] + os: [darwin] + + '@vscode/vsce-sign-darwin-x64@2.0.6': + resolution: {integrity: sha512-25GsUbTAiNfHSuRItoQafXOIpxlYj+IXb4/qarrXu7kmbH94jlm5sdWSCKrrREs8+GsXF1b+l3OB7VJy5jsykw==} + cpu: [x64] + os: [darwin] + + '@vscode/vsce-sign-linux-arm64@2.0.6': + resolution: {integrity: sha512-cfb1qK7lygtMa4NUl2582nP7aliLYuDEVpAbXJMkDq1qE+olIw/es+C8j1LJwvcRq1I2yWGtSn3EkDp9Dq5FdA==} + cpu: [arm64] + os: [linux] + + '@vscode/vsce-sign-linux-arm@2.0.6': + resolution: {integrity: sha512-UndEc2Xlq4HsuMPnwu7420uqceXjs4yb5W8E2/UkaHBB9OWCwMd3/bRe/1eLe3D8kPpxzcaeTyXiK3RdzS/1CA==} + cpu: [arm] + os: [linux] + + '@vscode/vsce-sign-linux-x64@2.0.6': + resolution: {integrity: sha512-/olerl1A4sOqdP+hjvJ1sbQjKN07Y3DVnxO4gnbn/ahtQvFrdhUi0G1VsZXDNjfqmXw57DmPi5ASnj/8PGZhAA==} + cpu: [x64] + os: [linux] + + '@vscode/vsce-sign-win32-arm64@2.0.6': + resolution: {integrity: sha512-ivM/MiGIY0PJNZBoGtlRBM/xDpwbdlCWomUWuLmIxbi1Cxe/1nooYrEQoaHD8ojVRgzdQEUzMsRbyF5cJJgYOg==} + cpu: [arm64] + os: [win32] + + '@vscode/vsce-sign-win32-x64@2.0.6': + resolution: {integrity: sha512-mgth9Kvze+u8CruYMmhHw6Zgy3GRX2S+Ed5oSokDEK5vPEwGGKnmuXua9tmFhomeAnhgJnL4DCna3TiNuGrBTQ==} + cpu: [x64] + os: [win32] + + '@vscode/vsce-sign@2.0.9': + resolution: {integrity: sha512-8IvaRvtFyzUnGGl3f5+1Cnor3LqaUWvhaUjAYO8Y39OUYlOf3cRd+dowuQYLpZcP3uwSG+mURwjEBOSq4SOJ0g==} + + '@vscode/vsce@2.32.0': + resolution: {integrity: sha512-3EFJfsgrSftIqt3EtdRcAygy/OJ3hstyI1cDmIgkU9CFZW5C+3djr6mfosndCUqcVYuyjmxOK1xmFp/Bq7+NIg==} + engines: {node: '>= 16'} + hasBin: true + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + azure-devops-node-api@12.5.0: + resolution: {integrity: sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + + brace-expansion@1.1.15: + resolution: {integrity: sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==} + + brace-expansion@2.1.1: + resolution: {integrity: sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==} + + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + + cheerio-select@2.1.0: + resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} + + cheerio@1.2.0: + resolution: {integrity: sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==} + engines: {node: '>=20.18.1'} + + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + cockatiel@3.2.1: + resolution: {integrity: sha512-gfrHV6ZPkquExvMh9IOkKsBzNDk6sDuZ6DdBGUBkvFnTCqCxzpuq48RySgP0AnaqQkw2zynOFj9yly6T1Q2G5Q==} + engines: {node: '>=16'} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@6.2.1: + resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} + engines: {node: '>= 6'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + css-select@5.2.2: + resolution: {integrity: sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==} + + css-what@6.2.2: + resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} + engines: {node: '>= 6'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + default-browser-id@5.0.1: + resolution: {integrity: sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==} + engines: {node: '>=18'} + + default-browser@5.5.0: + resolution: {integrity: sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==} + engines: {node: '>=18'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + + domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + + domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + + domutils@3.2.2: + resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + encoding-sniffer@0.2.1: + resolution: {integrity: sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + entities@2.1.0: + resolution: {integrity: sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} + engines: {node: '>= 6'} + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} + + htmlparser2@10.1.0: + resolution: {integrity: sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-wsl@3.1.1: + resolution: {integrity: sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==} + engines: {node: '>=16'} + + jsonc-parser@3.3.1: + resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} + + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + + keytar@7.9.0: + resolution: {integrity: sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ==} + + leven@3.1.0: + resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} + engines: {node: '>=6'} + + linkify-it@3.0.3: + resolution: {integrity: sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==} + + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + markdown-it@12.3.2: + resolution: {integrity: sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==} + hasBin: true + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mdurl@1.0.1: + resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + mute-stream@0.0.8: + resolution: {integrity: sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==} + + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + + node-abi@3.92.0: + resolution: {integrity: sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==} + engines: {node: '>=10'} + + node-addon-api@4.3.0: + resolution: {integrity: sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ==} + + nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + open@10.2.0: + resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} + engines: {node: '>=18'} + + parse-semver@1.1.1: + resolution: {integrity: sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ==} + + parse5-htmlparser2-tree-adapter@7.1.0: + resolution: {integrity: sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==} + + parse5-parser-stream@7.1.2: + resolution: {integrity: sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. + hasBin: true + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + qs@6.15.3: + resolution: {integrity: sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==} + engines: {node: '>=0.6'} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + read@1.0.7: + resolution: {integrity: sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==} + engines: {node: '>=0.8'} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sax@1.6.0: + resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} + engines: {node: '>=11.0.0'} + + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} + + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + + tar-fs@2.1.4: + resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tmp@0.2.7: + resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} + engines: {node: '>=14.14'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + tunnel@0.0.6: + resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} + engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} + + typed-rest-client@1.8.11: + resolution: {integrity: sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA==} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + uc.micro@1.0.6: + resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} + + underscore@1.13.8: + resolution: {integrity: sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + undici@7.28.0: + resolution: {integrity: sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==} + engines: {node: '>=20.18.1'} + + url-join@4.0.1: + resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vscode-jsonrpc@8.2.0: + resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==} + engines: {node: '>=14.0.0'} + + vscode-languageclient@9.0.1: + resolution: {integrity: sha512-JZiimVdvimEuHh5olxhxkht09m3JzUGwggb5eRUkzzJhZ2KjCN0nh55VfiED9oez9DyF8/fz1g1iBV3h+0Z2EA==} + engines: {vscode: ^1.82.0} + + vscode-languageserver-protocol@3.17.5: + resolution: {integrity: sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==} + + vscode-languageserver-types@3.17.5: + resolution: {integrity: sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==} + + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + wsl-utils@0.1.0: + resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} + engines: {node: '>=18'} + + xml2js@0.5.0: + resolution: {integrity: sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==} + engines: {node: '>=4.0.0'} + + xmlbuilder@11.0.1: + resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} + engines: {node: '>=4.0'} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + + yazl@2.5.1: + resolution: {integrity: sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw==} + +snapshots: + + '@azure/abort-controller@2.1.2': + dependencies: + tslib: 2.8.1 + + '@azure/core-auth@1.10.1': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-util': 1.13.1 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-client@1.10.2': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-rest-pipeline': 1.24.0 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-rest-pipeline@1.24.0': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + '@typespec/ts-http-runtime': 0.3.6 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-tracing@1.3.1': + dependencies: + tslib: 2.8.1 + + '@azure/core-util@1.13.1': + dependencies: + '@azure/abort-controller': 2.1.2 + '@typespec/ts-http-runtime': 0.3.6 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/identity@4.13.1': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-client': 1.10.2 + '@azure/core-rest-pipeline': 1.24.0 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + '@azure/msal-browser': 5.15.0 + '@azure/msal-node': 5.3.0 + open: 10.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/logger@1.3.0': + dependencies: + '@typespec/ts-http-runtime': 0.3.6 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/msal-browser@5.15.0': + dependencies: + '@azure/msal-common': 16.10.0 + + '@azure/msal-common@16.10.0': {} + + '@azure/msal-node@5.3.0': + dependencies: + '@azure/msal-common': 16.10.0 + jsonwebtoken: 9.0.3 + + '@types/node@20.19.43': + dependencies: + undici-types: 6.21.0 + + '@types/vscode@1.125.0': {} + + '@typespec/ts-http-runtime@0.3.6': + dependencies: + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@vscode/vsce-sign-alpine-arm64@2.0.6': + optional: true + + '@vscode/vsce-sign-alpine-x64@2.0.6': + optional: true + + '@vscode/vsce-sign-darwin-arm64@2.0.6': + optional: true + + '@vscode/vsce-sign-darwin-x64@2.0.6': + optional: true + + '@vscode/vsce-sign-linux-arm64@2.0.6': + optional: true + + '@vscode/vsce-sign-linux-arm@2.0.6': + optional: true + + '@vscode/vsce-sign-linux-x64@2.0.6': + optional: true + + '@vscode/vsce-sign-win32-arm64@2.0.6': + optional: true + + '@vscode/vsce-sign-win32-x64@2.0.6': + optional: true + + '@vscode/vsce-sign@2.0.9': + optionalDependencies: + '@vscode/vsce-sign-alpine-arm64': 2.0.6 + '@vscode/vsce-sign-alpine-x64': 2.0.6 + '@vscode/vsce-sign-darwin-arm64': 2.0.6 + '@vscode/vsce-sign-darwin-x64': 2.0.6 + '@vscode/vsce-sign-linux-arm': 2.0.6 + '@vscode/vsce-sign-linux-arm64': 2.0.6 + '@vscode/vsce-sign-linux-x64': 2.0.6 + '@vscode/vsce-sign-win32-arm64': 2.0.6 + '@vscode/vsce-sign-win32-x64': 2.0.6 + + '@vscode/vsce@2.32.0': + dependencies: + '@azure/identity': 4.13.1 + '@vscode/vsce-sign': 2.0.9 + azure-devops-node-api: 12.5.0 + chalk: 2.4.2 + cheerio: 1.2.0 + cockatiel: 3.2.1 + commander: 6.2.1 + form-data: 4.0.6 + glob: 7.2.3 + hosted-git-info: 4.1.0 + jsonc-parser: 3.3.1 + leven: 3.1.0 + markdown-it: 12.3.2 + mime: 1.6.0 + minimatch: 3.1.5 + parse-semver: 1.1.1 + read: 1.0.7 + semver: 7.8.5 + tmp: 0.2.7 + typed-rest-client: 1.8.11 + url-join: 4.0.1 + xml2js: 0.5.0 + yauzl: 2.10.0 + yazl: 2.5.1 + optionalDependencies: + keytar: 7.9.0 + transitivePeerDependencies: + - supports-color + + agent-base@7.1.4: {} + + ansi-styles@3.2.1: + dependencies: + color-convert: 1.9.3 + + argparse@2.0.1: {} + + asynckit@0.4.0: {} + + azure-devops-node-api@12.5.0: + dependencies: + tunnel: 0.0.6 + typed-rest-client: 1.8.11 + + balanced-match@1.0.2: {} + + base64-js@1.5.1: + optional: true + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + optional: true + + boolbase@1.0.0: {} + + brace-expansion@1.1.15: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.1.1: + dependencies: + balanced-match: 1.0.2 + + buffer-crc32@0.2.13: {} + + buffer-equal-constant-time@1.0.1: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + optional: true + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + chalk@2.4.2: + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + + cheerio-select@2.1.0: + dependencies: + boolbase: 1.0.0 + css-select: 5.2.2 + css-what: 6.2.2 + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + + cheerio@1.2.0: + dependencies: + cheerio-select: 2.1.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + domutils: 3.2.2 + encoding-sniffer: 0.2.1 + htmlparser2: 10.1.0 + parse5: 7.3.0 + parse5-htmlparser2-tree-adapter: 7.1.0 + parse5-parser-stream: 7.1.2 + undici: 7.28.0 + whatwg-mimetype: 4.0.0 + + chownr@1.1.4: + optional: true + + cockatiel@3.2.1: {} + + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + + color-name@1.1.3: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@6.2.1: {} + + concat-map@0.0.1: {} + + css-select@5.2.2: + dependencies: + boolbase: 1.0.0 + css-what: 6.2.2 + domhandler: 5.0.3 + domutils: 3.2.2 + nth-check: 2.1.1 + + css-what@6.2.2: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + optional: true + + deep-extend@0.6.0: + optional: true + + default-browser-id@5.0.1: {} + + default-browser@5.5.0: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.1 + + define-lazy-prop@3.0.0: {} + + delayed-stream@1.0.0: {} + + detect-libc@2.1.2: + optional: true + + dom-serializer@2.0.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + + domelementtype@2.3.0: {} + + domhandler@5.0.3: + dependencies: + domelementtype: 2.3.0 + + domutils@3.2.2: + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + encoding-sniffer@0.2.1: + dependencies: + iconv-lite: 0.6.3 + whatwg-encoding: 3.1.1 + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + optional: true + + entities@2.1.0: {} + + entities@4.5.0: {} + + entities@6.0.1: {} + + entities@7.0.1: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + escape-string-regexp@1.0.5: {} + + expand-template@2.0.3: + optional: true + + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + + form-data@4.0.6: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 + + fs-constants@1.0.0: + optional: true + + fs.realpath@1.0.0: {} + + function-bind@1.1.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + github-from-package@0.0.0: + optional: true + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + + gopd@1.2.0: {} + + has-flag@3.0.0: {} + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + hosted-git-info@4.1.0: + dependencies: + lru-cache: 6.0.0 + + htmlparser2@10.1.0: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 7.0.1 + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: + optional: true + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + ini@1.3.8: + optional: true + + is-docker@3.0.0: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-wsl@3.1.1: + dependencies: + is-inside-container: 1.0.0 + + jsonc-parser@3.3.1: {} + + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.8.5 + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + + keytar@7.9.0: + dependencies: + node-addon-api: 4.3.0 + prebuild-install: 7.1.3 + optional: true + + leven@3.1.0: {} + + linkify-it@3.0.3: + dependencies: + uc.micro: 1.0.6 + + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + + lodash.once@4.1.1: {} + + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + + markdown-it@12.3.2: + dependencies: + argparse: 2.0.1 + entities: 2.1.0 + linkify-it: 3.0.3 + mdurl: 1.0.1 + uc.micro: 1.0.6 + + math-intrinsics@1.1.0: {} + + mdurl@1.0.1: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@1.6.0: {} + + mimic-response@3.1.0: + optional: true + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.15 + + minimatch@5.1.9: + dependencies: + brace-expansion: 2.1.1 + + minimist@1.2.8: + optional: true + + mkdirp-classic@0.5.3: + optional: true + + ms@2.1.3: {} + + mute-stream@0.0.8: {} + + napi-build-utils@2.0.0: + optional: true + + node-abi@3.92.0: + dependencies: + semver: 7.8.5 + optional: true + + node-addon-api@4.3.0: + optional: true + + nth-check@2.1.1: + dependencies: + boolbase: 1.0.0 + + object-inspect@1.13.4: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + open@10.2.0: + dependencies: + default-browser: 5.5.0 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + wsl-utils: 0.1.0 + + parse-semver@1.1.1: + dependencies: + semver: 5.7.2 + + parse5-htmlparser2-tree-adapter@7.1.0: + dependencies: + domhandler: 5.0.3 + parse5: 7.3.0 + + parse5-parser-stream@7.1.2: + dependencies: + parse5: 7.3.0 + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + path-is-absolute@1.0.1: {} + + pend@1.2.0: {} + + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.1.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.92.0 + pump: 3.0.4 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.4 + tunnel-agent: 0.6.0 + optional: true + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + optional: true + + qs@6.15.3: + dependencies: + es-define-property: 1.0.1 + side-channel: 1.1.1 + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + optional: true + + read@1.0.7: + dependencies: + mute-stream: 0.0.8 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + optional: true + + run-applescript@7.1.0: {} + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + sax@1.6.0: {} + + semver@5.7.2: {} + + semver@7.8.5: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + simple-concat@1.0.1: + optional: true + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + optional: true + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + optional: true + + strip-json-comments@2.0.1: + optional: true + + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + + tar-fs@2.1.4: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.4 + tar-stream: 2.2.0 + optional: true + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + optional: true + + tmp@0.2.7: {} + + tslib@2.8.1: {} + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + optional: true + + tunnel@0.0.6: {} + + typed-rest-client@1.8.11: + dependencies: + qs: 6.15.3 + tunnel: 0.0.6 + underscore: 1.13.8 + + typescript@5.9.3: {} + + uc.micro@1.0.6: {} + + underscore@1.13.8: {} + + undici-types@6.21.0: {} + + undici@7.28.0: {} + + url-join@4.0.1: {} + + util-deprecate@1.0.2: + optional: true + + vscode-jsonrpc@8.2.0: {} + + vscode-languageclient@9.0.1: + dependencies: + minimatch: 5.1.9 + semver: 7.8.5 + vscode-languageserver-protocol: 3.17.5 + + vscode-languageserver-protocol@3.17.5: + dependencies: + vscode-jsonrpc: 8.2.0 + vscode-languageserver-types: 3.17.5 + + vscode-languageserver-types@3.17.5: {} + + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + + whatwg-mimetype@4.0.0: {} + + wrappy@1.0.2: {} + + wsl-utils@0.1.0: + dependencies: + is-wsl: 3.1.1 + + xml2js@0.5.0: + dependencies: + sax: 1.6.0 + xmlbuilder: 11.0.1 + + xmlbuilder@11.0.1: {} + + yallist@4.0.0: {} + + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + + yazl@2.5.1: + dependencies: + buffer-crc32: 0.2.13 diff --git a/editors/vscode/pnpm-workspace.yaml b/editors/vscode/pnpm-workspace.yaml new file mode 100644 index 0000000..02de15a --- /dev/null +++ b/editors/vscode/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +allowBuilds: + '@vscode/vsce-sign': true + keytar: true diff --git a/editors/vscode/src/extension.ts b/editors/vscode/src/extension.ts new file mode 100644 index 0000000..c8dc056 --- /dev/null +++ b/editors/vscode/src/extension.ts @@ -0,0 +1,105 @@ +import * as cp from 'child_process'; +import * as path from 'path'; +import * as vscode from 'vscode'; +import { + LanguageClient, + LanguageClientOptions, + ServerOptions, + TransportKind, +} from 'vscode-languageclient/node'; + +let client: LanguageClient | undefined; +let outputChannel: vscode.OutputChannel; + +export function activate(context: vscode.ExtensionContext): void { + outputChannel = vscode.window.createOutputChannel('PgTidy'); + context.subscriptions.push(outputChannel); + + // Commands are always registered so they work even when the LSP is disabled. + context.subscriptions.push( + vscode.commands.registerCommand('pgtidy.showVersion', cmdShowVersion), + vscode.commands.registerCommand('pgtidy.showConfig', cmdShowConfig), + vscode.commands.registerCommand('pgtidy.formatDocument', cmdFormatDocument), + ); + + const cfg = vscode.workspace.getConfiguration('pgtidy'); + if (!cfg.get('enable', true)) { + return; + } + + const bin = cfg.get('path', 'pgtidy'); + + const serverOptions: ServerOptions = { + command: bin, + args: ['lsp'], + transport: TransportKind.stdio, + }; + + const clientOptions: LanguageClientOptions = { + documentSelector: [{ scheme: 'file', language: 'sql' }], + synchronize: { + fileEvents: vscode.workspace.createFileSystemWatcher('**/*.{sql,pgsql}'), + }, + }; + + client = new LanguageClient('pgtidy', 'PgTidy', serverOptions, clientOptions); + context.subscriptions.push(client); + client.start(); +} + +export function deactivate(): Thenable | undefined { + return client?.stop(); +} + +function binary(): string { + return vscode.workspace.getConfiguration('pgtidy').get('path', 'pgtidy'); +} + +function contextDir(): string | undefined { + const folders = vscode.workspace.workspaceFolders; + if (folders && folders.length > 0) { + return folders[0].uri.fsPath; + } + const doc = vscode.window.activeTextEditor?.document; + if (doc && !doc.isUntitled) { + return path.dirname(doc.uri.fsPath); + } + return undefined; +} + +function cmdShowVersion(): void { + cp.execFile(binary(), ['version'], (err, stdout, stderr) => { + if (err) { + vscode.window.showErrorMessage(`PgTidy: ${stderr.trim() || err.message}`); + return; + } + vscode.window.showInformationMessage(stdout.trim()); + }); +} + +function cmdShowConfig(): void { + const cwd = contextDir(); + cp.execFile(binary(), ['config'], { cwd }, (err, stdout, stderr) => { + if (err) { + vscode.window.showErrorMessage(`PgTidy: ${stderr.trim() || err.message}`); + return; + } + outputChannel.clear(); + outputChannel.appendLine('PgTidy — effective configuration'); + if (cwd) { + outputChannel.appendLine(`(resolved from: ${cwd})`); + } + outputChannel.appendLine(''); + outputChannel.append(stdout); + outputChannel.show(true); + }); +} + +async function cmdFormatDocument(): Promise { + const editor = vscode.window.activeTextEditor; + if (!editor) { + vscode.window.showWarningMessage('PgTidy: no active editor'); + return; + } + await vscode.commands.executeCommand('editor.action.formatDocument'); +} diff --git a/editors/vscode/tsconfig.json b/editors/vscode/tsconfig.json new file mode 100644 index 0000000..fd42190 --- /dev/null +++ b/editors/vscode/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "ES2020", + "outDir": "out", + "lib": ["ES2020"], + "sourceMap": true, + "rootDir": "src", + "strict": true, + "skipLibCheck": true + }, + "exclude": ["node_modules", ".vscode-test"] +} diff --git a/go.mod b/go.mod index 736454d..0db9f40 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/hein/pgtidy +module git.warky.dev/wdevs/pgtidy go 1.26 diff --git a/pkg/cst/cst.go b/pkg/cst/cst.go index 5355061..f2c9c51 100644 --- a/pkg/cst/cst.go +++ b/pkg/cst/cst.go @@ -11,7 +11,7 @@ package cst import ( "strings" - "github.com/hein/pgtidy/pkg/lexer" + "git.warky.dev/wdevs/pgtidy/pkg/lexer" ) // Trivia is a run of whitespace/comment tokens preceding a significant token. diff --git a/pkg/diagnostics/diagnostics.go b/pkg/diagnostics/diagnostics.go index ed35b00..e46a5ed 100644 --- a/pkg/diagnostics/diagnostics.go +++ b/pkg/diagnostics/diagnostics.go @@ -12,6 +12,15 @@ const ( SeverityHint Severity = "hint" ) +// TextFix is a byte-range replacement that can be applied to the source SQL. +// Replace src[Offset:End] with New. An insertion has Offset == End. +type TextFix struct { + Offset int // byte offset in source (inclusive) + End int // byte offset in source (exclusive) + New string // replacement text + Title string // short description shown in editor UI +} + // Diagnostic is a single lint finding. type Diagnostic struct { // RuleID is the stable identifier for the rule that produced this finding @@ -27,4 +36,6 @@ type Diagnostic struct { Line int // Col is the 1-based column number of the finding. Col int + // Fix is non-nil when an autofix is available for this diagnostic. + Fix *TextFix } diff --git a/pkg/format/body.go b/pkg/format/body.go index b8103c7..c7390f5 100644 --- a/pkg/format/body.go +++ b/pkg/format/body.go @@ -3,9 +3,9 @@ package format import ( "strings" - "github.com/hein/pgtidy/pkg/config" - "github.com/hein/pgtidy/pkg/cst" - "github.com/hein/pgtidy/pkg/lexer" + "git.warky.dev/wdevs/pgtidy/pkg/config" + "git.warky.dev/wdevs/pgtidy/pkg/cst" + "git.warky.dev/wdevs/pgtidy/pkg/lexer" ) // sqlClauseKw: col-0 lines at paren-depth 0 starting with these keywords stay diff --git a/pkg/format/format.go b/pkg/format/format.go index afe2153..cdb6223 100644 --- a/pkg/format/format.go +++ b/pkg/format/format.go @@ -14,9 +14,9 @@ package format import ( "strings" - "github.com/hein/pgtidy/pkg/config" - "github.com/hein/pgtidy/pkg/cst" - "github.com/hein/pgtidy/pkg/lexer" + "git.warky.dev/wdevs/pgtidy/pkg/config" + "git.warky.dev/wdevs/pgtidy/pkg/cst" + "git.warky.dev/wdevs/pgtidy/pkg/lexer" ) // File formats a parsed file with the given style. diff --git a/pkg/format/format_test.go b/pkg/format/format_test.go index 50f722e..a38f584 100644 --- a/pkg/format/format_test.go +++ b/pkg/format/format_test.go @@ -6,9 +6,9 @@ import ( "strings" "testing" - "github.com/hein/pgtidy/pkg/config" - "github.com/hein/pgtidy/pkg/lexer" - "github.com/hein/pgtidy/pkg/parser" + "git.warky.dev/wdevs/pgtidy/pkg/config" + "git.warky.dev/wdevs/pgtidy/pkg/lexer" + "git.warky.dev/wdevs/pgtidy/pkg/parser" ) func format(src string) string { diff --git a/pkg/lint/fix.go b/pkg/lint/fix.go new file mode 100644 index 0000000..24dfd18 --- /dev/null +++ b/pkg/lint/fix.go @@ -0,0 +1,39 @@ +package lint + +import ( + "sort" + + "git.warky.dev/wdevs/pgtidy/pkg/diagnostics" +) + +// ApplyFixes applies all autofixes from diags to src and returns the result. +// Fixes are applied in reverse-offset order so earlier edits do not shift the +// byte positions of later ones. Overlapping fixes are skipped. +func ApplyFixes(src string, diags []diagnostics.Diagnostic) string { + type fix struct { + offset, end int + new string + } + var fixes []fix + for _, d := range diags { + if d.Fix != nil { + fixes = append(fixes, fix{d.Fix.Offset, d.Fix.End, d.Fix.New}) + } + } + if len(fixes) == 0 { + return src + } + sort.Slice(fixes, func(i, j int) bool { + return fixes[i].offset > fixes[j].offset + }) + b := []byte(src) + last := len(b) + 1 // sentinel: no fix applied yet + for _, f := range fixes { + if f.end > last { + continue // overlaps a previously applied fix; skip + } + last = f.offset + b = append(b[:f.offset], append([]byte(f.new), b[f.end:]...)...) + } + return string(b) +} diff --git a/pkg/lint/fix_test.go b/pkg/lint/fix_test.go new file mode 100644 index 0000000..4d9f7ae --- /dev/null +++ b/pkg/lint/fix_test.go @@ -0,0 +1,104 @@ +package lint_test + +import ( + "strings" + "testing" + + "git.warky.dev/wdevs/pgtidy/pkg/lint" +) + +func TestMIG001Fix(t *testing.T) { + src := "CREATE INDEX idx_orders_user ON orders(user_id);" + eng := lint.New() + diags, _ := eng.Check(src, "test.sql") + + var hasFix bool + for _, d := range diags { + if d.RuleID == "MIG001" && d.Fix != nil { + hasFix = true + } + } + if !hasFix { + t.Fatal("MIG001 diagnostic missing Fix") + } + + fixed := lint.ApplyFixes(src, diags) + if !strings.Contains(fixed, "CONCURRENTLY") { + t.Errorf("fix did not insert CONCURRENTLY; got: %s", fixed) + } + // Re-check: MIG001 should be gone + diags2, _ := eng.Check(fixed, "test.sql") + for _, d := range diags2 { + if d.RuleID == "MIG001" { + t.Errorf("MIG001 still fires after fix: %s", fixed) + } + } +} + +func TestMIG003Fix(t *testing.T) { + cases := []struct { + name string + src string + }{ + { + "FK constraint", + "ALTER TABLE orders ADD CONSTRAINT fk_orders_user FOREIGN KEY (user_id) REFERENCES users(id);", + }, + { + "CHECK constraint", + "ALTER TABLE orders ADD CONSTRAINT chk_positive CHECK (amount > 0);", + }, + } + eng := lint.New() + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + diags, _ := eng.Check(tc.src, "test.sql") + var hasFix bool + for _, d := range diags { + if d.RuleID == "MIG003" && d.Fix != nil { + hasFix = true + } + } + if !hasFix { + t.Fatal("MIG003 diagnostic missing Fix") + } + fixed := lint.ApplyFixes(tc.src, diags) + if !strings.Contains(fixed, "NOT VALID") { + t.Errorf("fix did not insert NOT VALID; got: %s", fixed) + } + // Re-check: MIG003 should be gone + diags2, _ := eng.Check(fixed, "test.sql") + for _, d := range diags2 { + if d.RuleID == "MIG003" { + t.Errorf("MIG003 still fires after fix: %s", fixed) + } + } + }) + } +} + +func TestApplyFixes_MultipleInOneFile(t *testing.T) { + src := `CREATE INDEX a ON t(x); +ALTER TABLE t ADD CONSTRAINT fk FOREIGN KEY (x) REFERENCES u(id);` + + eng := lint.New() + diags, _ := eng.Check(src, "test.sql") + fixed := lint.ApplyFixes(src, diags) + + if !strings.Contains(fixed, "CONCURRENTLY") { + t.Error("CONCURRENTLY missing after multi-fix") + } + if !strings.Contains(fixed, "NOT VALID") { + t.Error("NOT VALID missing after multi-fix") + } +} + +func TestApplyFixes_NoFixes(t *testing.T) { + src := "SELECT 1;" + eng := lint.New() + diags, _ := eng.Check(src, "test.sql") + fixed := lint.ApplyFixes(src, diags) + if fixed != src { + t.Errorf("ApplyFixes changed unfixable source: %q", fixed) + } +} diff --git a/pkg/lint/lint.go b/pkg/lint/lint.go index d844f8e..9589571 100644 --- a/pkg/lint/lint.go +++ b/pkg/lint/lint.go @@ -11,8 +11,8 @@ import ( pg_query "github.com/pganalyze/pg_query_go/v6" - "github.com/hein/pgtidy/pkg/diagnostics" - "github.com/hein/pgtidy/pkg/pgast" + "git.warky.dev/wdevs/pgtidy/pkg/diagnostics" + "git.warky.dev/wdevs/pgtidy/pkg/pgast" ) // Rule is implemented by each lint rule. diff --git a/pkg/lint/lint_test.go b/pkg/lint/lint_test.go index c68fad0..01b1258 100644 --- a/pkg/lint/lint_test.go +++ b/pkg/lint/lint_test.go @@ -5,8 +5,8 @@ import ( "path/filepath" "testing" - "github.com/hein/pgtidy/pkg/diagnostics" - "github.com/hein/pgtidy/pkg/lint" + "git.warky.dev/wdevs/pgtidy/pkg/diagnostics" + "git.warky.dev/wdevs/pgtidy/pkg/lint" ) func fixtureDir() string { diff --git a/pkg/lint/rules_correctness.go b/pkg/lint/rules_correctness.go index bc00d64..79a3737 100644 --- a/pkg/lint/rules_correctness.go +++ b/pkg/lint/rules_correctness.go @@ -5,8 +5,8 @@ import ( pg_query "github.com/pganalyze/pg_query_go/v6" - "github.com/hein/pgtidy/pkg/diagnostics" - "github.com/hein/pgtidy/pkg/pgast" + "git.warky.dev/wdevs/pgtidy/pkg/diagnostics" + "git.warky.dev/wdevs/pgtidy/pkg/pgast" ) // COR001 — SELECT *. diff --git a/pkg/lint/rules_migration.go b/pkg/lint/rules_migration.go index 2b57d0a..bfb214f 100644 --- a/pkg/lint/rules_migration.go +++ b/pkg/lint/rules_migration.go @@ -2,11 +2,12 @@ package lint import ( "fmt" + "strings" pg_query "github.com/pganalyze/pg_query_go/v6" - "github.com/hein/pgtidy/pkg/diagnostics" - "github.com/hein/pgtidy/pkg/pgast" + "git.warky.dev/wdevs/pgtidy/pkg/diagnostics" + "git.warky.dev/wdevs/pgtidy/pkg/pgast" ) // MIG001 — CREATE INDEX without CONCURRENT. @@ -30,13 +31,17 @@ func (ruleMIG001) Check(stmts []*pg_query.RawStmt, src string) []diagnostics.Dia continue } line, col := pgast.LocationToLineCol(src, pgast.FirstTokenOffset(src, int(raw.StmtLocation))) - out = append(out, diagnostics.Diagnostic{ + d := diagnostics.Diagnostic{ RuleID: "MIG001", Severity: diagnostics.SeverityWarning, Message: fmt.Sprintf("CREATE INDEX on %q without CONCURRENT blocks writes; use CREATE INDEX CONCURRENTLY", relName(s.Relation)), Line: line, Col: col, - }) + } + if fix := mig001Fix(src, int(raw.StmtLocation), int(raw.StmtLen)); fix != nil { + d.Fix = fix + } + out = append(out, d) } return out } @@ -127,13 +132,17 @@ func (ruleMIG003) Check(stmts []*pg_query.RawStmt, src string) []diagnostics.Dia kind = "CHECK" } line, col := pgast.LocationToLineCol(src, pgast.FirstTokenOffset(src, int(raw.StmtLocation))) - out = append(out, diagnostics.Diagnostic{ + d := diagnostics.Diagnostic{ RuleID: "MIG003", Severity: diagnostics.SeverityWarning, Message: fmt.Sprintf("ALTER TABLE %q ADD %s CONSTRAINT without NOT VALID validates all rows immediately; use NOT VALID + VALIDATE CONSTRAINT", relName(tbl.Relation), kind), Line: line, Col: col, - }) + } + if fix := mig003Fix(src, int(raw.StmtLocation), int(raw.StmtLen)); fix != nil { + d.Fix = fix + } + out = append(out, d) } } return out @@ -169,3 +178,63 @@ func relName(rv *pg_query.RangeVar) string { } return rv.Relname } + +// stmtText returns the text of a statement given its start offset and length. +// When stmtLen is 0 (last statement in file) it extends to EOF. +func stmtText(src string, stmtOffset, stmtLen int) string { + end := stmtOffset + stmtLen + if stmtLen == 0 || end > len(src) { + end = len(src) + } + return src[stmtOffset:end] +} + +// mig001Fix builds the TextFix for MIG001: insert CONCURRENTLY after INDEX. +func mig001Fix(src string, stmtOffset, stmtLen int) *diagnostics.TextFix { + text := stmtText(src, stmtOffset, stmtLen) + upper := strings.ToUpper(text) + pos := strings.Index(upper, "INDEX") + if pos < 0 { + return nil + } + insertAt := stmtOffset + pos + len("INDEX") + return &diagnostics.TextFix{ + Offset: insertAt, + End: insertAt, + New: " CONCURRENTLY", + Title: "Add CONCURRENTLY", + } +} + +// mig003Fix builds the TextFix for MIG003: insert NOT VALID before the trailing semicolon. +// pg_query's StmtLen excludes the ";", which sits at src[stmtOffset+stmtLen]. +func mig003Fix(src string, stmtOffset, stmtLen int) *diagnostics.TextFix { + end := stmtOffset + stmtLen + semiAt := -1 + if stmtLen > 0 && end < len(src) && src[end] == ';' { + semiAt = end + } else { + // Fallback for stmtLen=0 (last statement without terminator) or edge cases. + if stmtLen == 0 { + end = len(src) + } + idx := strings.LastIndex(src[stmtOffset:end], ";") + if idx >= 0 { + semiAt = stmtOffset + idx + } + } + if semiAt < 0 { + return nil + } + // Insert " NOT VALID" just before the ";", after any trailing whitespace. + insertAt := semiAt + for insertAt > stmtOffset && (src[insertAt-1] == ' ' || src[insertAt-1] == '\t' || src[insertAt-1] == '\n' || src[insertAt-1] == '\r') { + insertAt-- + } + return &diagnostics.TextFix{ + Offset: insertAt, + End: insertAt, + New: " NOT VALID", + Title: "Add NOT VALID", + } +} diff --git a/pkg/lint/rules_naming.go b/pkg/lint/rules_naming.go index e528d79..27f64c7 100644 --- a/pkg/lint/rules_naming.go +++ b/pkg/lint/rules_naming.go @@ -7,8 +7,8 @@ import ( pg_query "github.com/pganalyze/pg_query_go/v6" - "github.com/hein/pgtidy/pkg/diagnostics" - "github.com/hein/pgtidy/pkg/pgast" + "git.warky.dev/wdevs/pgtidy/pkg/diagnostics" + "git.warky.dev/wdevs/pgtidy/pkg/pgast" ) // reSnakeCase matches valid snake_case identifiers: lowercase letters, digits, diff --git a/pkg/lsp/server.go b/pkg/lsp/server.go new file mode 100644 index 0000000..ca14d2e --- /dev/null +++ b/pkg/lsp/server.go @@ -0,0 +1,429 @@ +// Package lsp implements a Language Server Protocol server for PgTidy. +// +// The server communicates over stdio using JSON-RPC 2.0 with Content-Length +// framing. It provides: +// - textDocument/formatting — full-document formatting via pkg/format +// - textDocument/publishDiagnostics — lint findings via pkg/lint, sent on +// every didOpen/didChange notification +package lsp + +import ( + "bufio" + "context" + "encoding/json" + "fmt" + "io" + "strconv" + "strings" + + "git.warky.dev/wdevs/pgtidy/pkg/config" + "git.warky.dev/wdevs/pgtidy/pkg/diagnostics" + "git.warky.dev/wdevs/pgtidy/pkg/format" + "git.warky.dev/wdevs/pgtidy/pkg/lint" + "git.warky.dev/wdevs/pgtidy/pkg/parser" +) + +// Serve runs the LSP server, reading JSON-RPC messages from r and writing to w. +// startDir is used for .pgtidy.yaml discovery. Returns when the client sends +// "exit" or r reaches EOF. +func Serve(ctx context.Context, r io.Reader, w io.Writer, startDir string) error { + cfg, _ := config.Load(startDir) + srv := &server{ + docs: make(map[string]string), + fixes: make(map[string][]diagnostics.Diagnostic), + cfg: cfg, + w: w, + } + return srv.loop(ctx, r) +} + +type server struct { + docs map[string]string // URI → current text + fixes map[string][]diagnostics.Diagnostic // URI → diagnostics that have fixes + cfg config.Style + w io.Writer +} + +func (s *server) loop(ctx context.Context, r io.Reader) error { + br := bufio.NewReader(r) + for { + select { + case <-ctx.Done(): + return ctx.Err() + default: + } + raw, err := readMsg(br) + if err != nil { + if err == io.EOF { + return nil + } + return err + } + if s.handle(raw) { + return nil + } + } +} + +// handle dispatches one JSON-RPC message. Returns true when the server should exit. +func (s *server) handle(raw []byte) bool { + var req rpcMsg + if err := json.Unmarshal(raw, &req); err != nil { + return false + } + + switch req.Method { + case "initialize": + s.reply(req.ID, initResult{ + Capabilities: serverCaps{ + TextDocumentSync: 1, // full sync + DocumentFormattingProvider: true, + CodeActionProvider: true, + }, + }) + case "initialized": // no-op notification + case "shutdown": + s.reply(req.ID, nil) + case "exit": + return true + case "textDocument/didOpen": + var p didOpenParams + json.Unmarshal(req.Params, &p) + s.docs[p.TextDocument.URI] = p.TextDocument.Text + s.pushDiagnostics(p.TextDocument.URI, p.TextDocument.Text) + case "textDocument/didChange": + var p didChangeParams + json.Unmarshal(req.Params, &p) + if len(p.ContentChanges) > 0 { + text := p.ContentChanges[len(p.ContentChanges)-1].Text + s.docs[p.TextDocument.URI] = text + s.pushDiagnostics(p.TextDocument.URI, text) + } + case "textDocument/didClose": + var p struct { + TextDocument textDocID `json:"textDocument"` + } + json.Unmarshal(req.Params, &p) + delete(s.docs, p.TextDocument.URI) + delete(s.fixes, p.TextDocument.URI) + s.notify("textDocument/publishDiagnostics", publishDiagnosticsParams{ + URI: p.TextDocument.URI, + Diagnostics: []lspDiagnostic{}, + }) + case "textDocument/formatting": + var p formattingParams + json.Unmarshal(req.Params, &p) + text, ok := s.docs[p.TextDocument.URI] + if !ok { + s.reply(req.ID, []textEdit{}) + return false + } + formatted := format.File(parser.Parse(text), s.cfg) + if formatted == text { + s.reply(req.ID, []textEdit{}) + return false + } + s.reply(req.ID, []textEdit{fullReplace(text, formatted)}) + case "textDocument/codeAction": + var p codeActionParams + json.Unmarshal(req.Params, &p) + s.handleCodeAction(req.ID, p) + case "$/cancelRequest": // ignore + default: + if req.ID != nil { + s.replyErr(req.ID, -32601, "method not found: "+req.Method) + } + } + return false +} + +func (s *server) pushDiagnostics(uri, text string) { + eng := lint.New() + diags, _ := eng.Check(text, uri) + + var fixable []diagnostics.Diagnostic + out := make([]lspDiagnostic, 0, len(diags)) + for _, d := range diags { + line := uint32(d.Line) + if line > 0 { + line-- + } + col := uint32(d.Col) + if col > 0 { + col-- + } + lspD := lspDiagnostic{ + Range: lspRange{Start: position{line, col}, End: position{line, col + 1}}, + Severity: severityCode(d.Severity), + Code: d.RuleID, + Source: "pgtidy", + Message: d.Message, + } + out = append(out, lspD) + if d.Fix != nil { + fixable = append(fixable, d) + } + } + s.fixes[uri] = fixable + s.notify("textDocument/publishDiagnostics", publishDiagnosticsParams{ + URI: uri, + Diagnostics: out, + }) +} + +func (s *server) handleCodeAction(id json.RawMessage, p codeActionParams) { + uri := p.TextDocument.URI + text, ok := s.docs[uri] + if !ok { + s.reply(id, []codeAction{}) + return + } + var actions []codeAction + for _, d := range s.fixes[uri] { + if d.Fix == nil { + continue + } + start := offsetToPosition(text, d.Fix.Offset) + end := offsetToPosition(text, d.Fix.End) + // Only include if the fix range overlaps the requested range. + if !rangesOverlap(start, end, p.Range.Start, p.Range.End) { + continue + } + actions = append(actions, codeAction{ + Title: d.Fix.Title, + Kind: "quickfix", + Edit: &workspaceEdit{ + Changes: map[string][]textEdit{ + uri: {{ + Range: lspRange{Start: start, End: end}, + NewText: d.Fix.New, + }}, + }, + }, + }) + } + s.reply(id, actions) +} + +// offsetToPosition converts a byte offset in text to an LSP position. +func offsetToPosition(text string, byteOff int) position { + if byteOff > len(text) { + byteOff = len(text) + } + line := uint32(0) + lastNL := -1 + for i := 0; i < byteOff; i++ { + if text[i] == '\n' { + line++ + lastNL = i + } + } + return position{line, uint32(byteOff - lastNL - 1)} +} + +// rangesOverlap returns true when [s1,e1) and [s2,e2) share any point. +// For insertions (s==e) we check if the point falls within the other range. +func rangesOverlap(s1, e1, s2, e2 position) bool { + cmp := func(a, b position) int { + if a.Line != b.Line { + if a.Line < b.Line { + return -1 + } + return 1 + } + if a.Character < b.Character { + return -1 + } + if a.Character > b.Character { + return 1 + } + return 0 + } + return cmp(s1, e2) <= 0 && cmp(s2, e1) <= 0 +} + +func severityCode(sev diagnostics.Severity) int { + switch sev { + case diagnostics.SeverityError: + return 1 + case diagnostics.SeverityWarning: + return 2 + case diagnostics.SeverityHint: + return 4 + } + return 3 // info +} + +// fullReplace builds a TextEdit that replaces the entire document. +func fullReplace(orig, formatted string) textEdit { + lines := strings.Split(orig, "\n") + lastLine := uint32(len(lines) - 1) + lastChar := uint32(len(lines[lastLine])) + return textEdit{ + Range: lspRange{Start: position{0, 0}, End: position{lastLine, lastChar}}, + NewText: formatted, + } +} + +// --- JSON-RPC 2.0 transport --- + +func readMsg(r *bufio.Reader) ([]byte, error) { + length := 0 + for { + line, err := r.ReadString('\n') + if err != nil { + return nil, err + } + line = strings.TrimRight(line, "\r\n") + if line == "" { + break + } + if after, ok := strings.CutPrefix(line, "Content-Length: "); ok { + length, _ = strconv.Atoi(after) + } + } + if length == 0 { + return nil, fmt.Errorf("lsp: missing Content-Length") + } + buf := make([]byte, length) + _, err := io.ReadFull(r, buf) + return buf, err +} + +func (s *server) send(v interface{}) { + data, err := json.Marshal(v) + if err != nil { + return + } + fmt.Fprintf(s.w, "Content-Length: %d\r\n\r\n", len(data)) + s.w.Write(data) //nolint:errcheck +} + +func (s *server) reply(id json.RawMessage, result interface{}) { + s.send(rpcResponse{JSONRPC: "2.0", ID: id, Result: result}) +} + +func (s *server) replyErr(id json.RawMessage, code int, msg string) { + s.send(rpcResponse{JSONRPC: "2.0", ID: id, Error: &rpcError{Code: code, Message: msg}}) +} + +func (s *server) notify(method string, params interface{}) { + s.send(rpcNotification{JSONRPC: "2.0", Method: method, Params: params}) +} + +// --- JSON-RPC 2.0 wire types --- + +type rpcMsg struct { + JSONRPC string `json:"jsonrpc"` + ID json.RawMessage `json:"id,omitempty"` + Method string `json:"method"` + Params json.RawMessage `json:"params,omitempty"` +} + +type rpcResponse struct { + JSONRPC string `json:"jsonrpc"` + ID json.RawMessage `json:"id"` + Result interface{} `json:"result"` + Error *rpcError `json:"error,omitempty"` +} + +type rpcNotification struct { + JSONRPC string `json:"jsonrpc"` + Method string `json:"method"` + Params interface{} `json:"params"` +} + +type rpcError struct { + Code int `json:"code"` + Message string `json:"message"` +} + +// --- LSP protocol types (minimal subset) --- + +type position struct { + Line uint32 `json:"line"` + Character uint32 `json:"character"` +} + +type lspRange struct { + Start position `json:"start"` + End position `json:"end"` +} + +type textEdit struct { + Range lspRange `json:"range"` + NewText string `json:"newText"` +} + +type initResult struct { + Capabilities serverCaps `json:"capabilities"` +} + +type serverCaps struct { + TextDocumentSync int `json:"textDocumentSync"` + DocumentFormattingProvider bool `json:"documentFormattingProvider"` + CodeActionProvider bool `json:"codeActionProvider"` +} + +type textDocItem struct { + URI string `json:"uri"` + LanguageID string `json:"languageId"` + Version int `json:"version"` + Text string `json:"text"` +} + +type textDocID struct { + URI string `json:"uri"` +} + +type contentChange struct { + Text string `json:"text"` +} + +type didOpenParams struct { + TextDocument textDocItem `json:"textDocument"` +} + +type didChangeParams struct { + TextDocument textDocID `json:"textDocument"` + ContentChanges []contentChange `json:"contentChanges"` +} + +type formattingParams struct { + TextDocument textDocID `json:"textDocument"` + Options struct { + TabSize int `json:"tabSize"` + InsertSpaces bool `json:"insertSpaces"` + } `json:"options"` +} + +type lspDiagnostic struct { + Range lspRange `json:"range"` + Severity int `json:"severity"` + Code string `json:"code,omitempty"` + Source string `json:"source,omitempty"` + Message string `json:"message"` +} + +type publishDiagnosticsParams struct { + URI string `json:"uri"` + Diagnostics []lspDiagnostic `json:"diagnostics"` +} + +type codeActionParams struct { + TextDocument textDocID `json:"textDocument"` + Range lspRange `json:"range"` + Context struct { + Diagnostics []lspDiagnostic `json:"diagnostics"` + } `json:"context"` +} + +type workspaceEdit struct { + Changes map[string][]textEdit `json:"changes"` +} + +type codeAction struct { + Title string `json:"title"` + Kind string `json:"kind,omitempty"` + Edit *workspaceEdit `json:"edit,omitempty"` +} diff --git a/pkg/lsp/server_test.go b/pkg/lsp/server_test.go new file mode 100644 index 0000000..911a4db --- /dev/null +++ b/pkg/lsp/server_test.go @@ -0,0 +1,200 @@ +package lsp + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "strings" + "testing" + "time" +) + +// rpc sends a JSON-RPC request frame and returns the raw body bytes. +func frame(id int, method string, params interface{}) []byte { + type req struct { + JSONRPC string `json:"jsonrpc"` + ID int `json:"id"` + Method string `json:"method"` + Params interface{} `json:"params"` + } + body, _ := json.Marshal(req{JSONRPC: "2.0", ID: id, Method: method, Params: params}) + return []byte(fmt.Sprintf("Content-Length: %d\r\n\r\n%s", len(body), body)) +} + +func notifFrame(method string, params interface{}) []byte { + type notif struct { + JSONRPC string `json:"jsonrpc"` + Method string `json:"method"` + Params interface{} `json:"params"` + } + body, _ := json.Marshal(notif{JSONRPC: "2.0", Method: method, Params: params}) + return []byte(fmt.Sprintf("Content-Length: %d\r\n\r\n%s", len(body), body)) +} + +// readResp reads one JSON-RPC response from a *bytes.Buffer (blocking until available). +func readResp(t *testing.T, buf *bytes.Buffer) map[string]interface{} { + t.Helper() + deadline := time.Now().Add(2 * time.Second) + for time.Now().Before(deadline) { + data := buf.Bytes() + // Find Content-Length header + idx := bytes.Index(data, []byte("Content-Length: ")) + if idx < 0 { + time.Sleep(5 * time.Millisecond) + continue + } + eol := bytes.Index(data[idx:], []byte("\r\n")) + if eol < 0 { + time.Sleep(5 * time.Millisecond) + continue + } + lenStr := string(data[idx+16 : idx+eol]) + var n int + fmt.Sscanf(lenStr, "%d", &n) + sep := bytes.Index(data, []byte("\r\n\r\n")) + if sep < 0 || len(data) < sep+4+n { + time.Sleep(5 * time.Millisecond) + continue + } + body := data[sep+4 : sep+4+n] + buf.Next(sep + 4 + n) + var result map[string]interface{} + json.Unmarshal(body, &result) + return result + } + t.Fatal("timeout waiting for response") + return nil +} + +func runServer(t *testing.T, input []byte) *bytes.Buffer { + t.Helper() + out := &bytes.Buffer{} + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + done := make(chan error, 1) + go func() { + done <- Serve(ctx, bytes.NewReader(input), out, t.TempDir()) + cancel() + }() + // Give the server time to process + select { + case <-done: + case <-time.After(3 * time.Second): + } + return out +} + +func TestInitialize(t *testing.T) { + input := append(frame(1, "initialize", map[string]interface{}{}), + notifFrame("initialized", map[string]interface{}{})...) + input = append(input, frame(2, "shutdown", nil)...) + input = append(input, notifFrame("exit", nil)...) + + out := runServer(t, input) + + resp := readResp(t, out) + if resp["id"].(float64) != 1 { + t.Fatalf("expected id=1, got %v", resp["id"]) + } + caps := resp["result"].(map[string]interface{})["capabilities"].(map[string]interface{}) + if caps["documentFormattingProvider"] != true { + t.Error("expected documentFormattingProvider=true") + } +} + +func TestFormatting(t *testing.T) { + unformatted := "create function foo() returns void language plpgsql as $$ begin end $$;" + uri := "file:///test.sql" + + var input []byte + input = append(input, frame(1, "initialize", map[string]interface{}{})...) + input = append(input, notifFrame("initialized", nil)...) + input = append(input, notifFrame("textDocument/didOpen", map[string]interface{}{ + "textDocument": map[string]interface{}{ + "uri": uri, + "languageId": "sql", + "version": 1, + "text": unformatted, + }, + })...) + input = append(input, frame(2, "textDocument/formatting", map[string]interface{}{ + "textDocument": map[string]interface{}{"uri": uri}, + "options": map[string]interface{}{"tabSize": 2, "insertSpaces": true}, + })...) + input = append(input, frame(3, "shutdown", nil)...) + input = append(input, notifFrame("exit", nil)...) + + out := runServer(t, input) + + // Skip initialize response and publishDiagnostics notification, find formatting response + var formattingResp map[string]interface{} + for i := 0; i < 10; i++ { + resp := readResp(t, out) + if resp == nil { + break + } + id, hasID := resp["id"] + if hasID && id.(float64) == 2 { + formattingResp = resp + break + } + } + if formattingResp == nil { + t.Fatal("did not receive formatting response") + } + result, ok := formattingResp["result"].([]interface{}) + if !ok { + t.Fatalf("expected array result, got %T: %v", formattingResp["result"], formattingResp["result"]) + } + if len(result) == 0 { + t.Fatal("expected at least one text edit") + } + edit := result[0].(map[string]interface{}) + newText := edit["newText"].(string) + if !strings.Contains(newText, "CREATE FUNCTION") { + t.Errorf("formatted output missing CREATE FUNCTION keyword; got:\n%s", newText) + } +} + +func TestDidClose_ClearsDiagnostics(t *testing.T) { + uri := "file:///test.sql" + // SQL with a lint violation (SELECT * triggers COR001) + sql := "SELECT * FROM users;" + + var input []byte + input = append(input, frame(1, "initialize", map[string]interface{}{})...) + input = append(input, notifFrame("initialized", nil)...) + input = append(input, notifFrame("textDocument/didOpen", map[string]interface{}{ + "textDocument": map[string]interface{}{ + "uri": uri, "languageId": "sql", "version": 1, "text": sql, + }, + })...) + input = append(input, notifFrame("textDocument/didClose", map[string]interface{}{ + "textDocument": map[string]interface{}{"uri": uri}, + })...) + input = append(input, frame(2, "shutdown", nil)...) + input = append(input, notifFrame("exit", nil)...) + + out := runServer(t, input) + + // Collect all publishDiagnostics notifications; the last one for this URI must be empty. + var lastDiags []interface{} + for i := 0; i < 20; i++ { + if out.Len() == 0 { + break + } + resp := readResp(t, out) + if resp == nil { + break + } + if resp["method"] == "textDocument/publishDiagnostics" { + p := resp["params"].(map[string]interface{}) + if p["uri"] == uri { + lastDiags = p["diagnostics"].([]interface{}) + } + } + } + if len(lastDiags) != 0 { + t.Errorf("expected empty diagnostics after didClose, got %d", len(lastDiags)) + } +} diff --git a/pkg/parser/parser.go b/pkg/parser/parser.go index 33504fc..341adca 100644 --- a/pkg/parser/parser.go +++ b/pkg/parser/parser.go @@ -8,8 +8,8 @@ package parser import ( - "github.com/hein/pgtidy/pkg/cst" - "github.com/hein/pgtidy/pkg/lexer" + "git.warky.dev/wdevs/pgtidy/pkg/cst" + "git.warky.dev/wdevs/pgtidy/pkg/lexer" ) // Parse lexes and parses src into a lossless cst.File. diff --git a/pkg/parser/parser_test.go b/pkg/parser/parser_test.go index b8bf7ba..505b654 100644 --- a/pkg/parser/parser_test.go +++ b/pkg/parser/parser_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/hein/pgtidy/pkg/cst" + "git.warky.dev/wdevs/pgtidy/pkg/cst" ) func TestParseRoundTripSmall(t *testing.T) {