From 5d93d8b364295737f29c437b11c905c0fff5c082 Mon Sep 17 00:00:00 2001 From: louiss0 Date: Thu, 21 May 2026 12:52:19 -0400 Subject: [PATCH 01/25] fix(cmd): stabilize cwd validation test in CI --- cmd/cmd_test.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go index 3f0308e..c415d30 100644 --- a/cmd/cmd_test.go +++ b/cmd/cmd_test.go @@ -353,14 +353,17 @@ var _ = Describe("JPD Commands", func() { }) It("should reject a --cwd flag value that does not end with '/'", func() { - // Use platform-appropriate invalid path + // Use a real POSIX directory in CI because folder-path validation is relaxed there, + // and command execution proceeds to target-directory resolution. var invalidPath string if runtime.GOOS == "windows" { - // On Windows, use POSIX-style path which should be invalid invalidPath = "/tmp/my-project" } else { - // On POSIX, use path without trailing slash - invalidPath = "/tmp/my-project" + tempDir, err := os.MkdirTemp("", "jpd-cwd-flag") + assert.NoError(err) + defer os.RemoveAll(tempDir) + + invalidPath = filepath.ToSlash(tempDir) } _, err := executeCmd(currentRootCmd, "--cwd", invalidPath) From e690a9cb40510ea2ec0ce6acf90e0a181a70bf3b Mon Sep 17 00:00:00 2001 From: louiss0 Date: Thu, 21 May 2026 13:48:40 -0400 Subject: [PATCH 02/25] fix(cmd): handle cwd test cleanup errors --- cmd/cmd_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go index c415d30..3e3c41d 100644 --- a/cmd/cmd_test.go +++ b/cmd/cmd_test.go @@ -361,7 +361,9 @@ var _ = Describe("JPD Commands", func() { } else { tempDir, err := os.MkdirTemp("", "jpd-cwd-flag") assert.NoError(err) - defer os.RemoveAll(tempDir) + defer func() { + assert.NoError(os.RemoveAll(tempDir)) + }() invalidPath = filepath.ToSlash(tempDir) } From ca4a5b036cf849b6043fa244811c3cb17f527716 Mon Sep 17 00:00:00 2001 From: louiss0 Date: Thu, 21 May 2026 15:48:45 -0400 Subject: [PATCH 03/25] fix(release): default docs version to latest tag --- .github/workflows/release.yml | 1 + docs/astro.config.mjs | 5 ++++- docs/src/components/ReleaseBadge.astro | 9 +++++++++ docs/src/content/docs/index.mdx | 5 +++-- 4 files changed, 17 insertions(+), 3 deletions(-) create mode 100644 docs/src/components/ReleaseBadge.astro diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a47e0e5..a508fc3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,6 +12,7 @@ permissions: env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + PUBLIC_JPD_VERSION: ${{ github.ref_name }} jobs: release: diff --git a/docs/astro.config.mjs b/docs/astro.config.mjs index bf5b433..4044f78 100644 --- a/docs/astro.config.mjs +++ b/docs/astro.config.mjs @@ -2,6 +2,9 @@ import { defineConfig } from "astro/config"; import starlight from "@astrojs/starlight"; +const releaseTag = process.env.PUBLIC_JPD_VERSION ?? "v4.0.0"; +const docsVersion = releaseTag.startsWith("v") ? releaseTag.slice(1) : releaseTag; + // https://astro.build/config export default defineConfig({ site: "https://jpd-docs.onrender.com", @@ -9,7 +12,7 @@ export default defineConfig({ starlight({ title: "javascript-package-delegator (jpd)", description: - "A universal JavaScript package manager CLI written in Go that delegates to npm, Yarn, pnpm, Bun, and Deno.", + `A universal JavaScript package manager CLI written in Go that delegates to npm, Yarn, pnpm, Bun, and Deno. Current docs version: ${docsVersion}.`, // SEO and social sharing configuration logo: { src: "./src/assets/jpd-logo.svg", diff --git a/docs/src/components/ReleaseBadge.astro b/docs/src/components/ReleaseBadge.astro new file mode 100644 index 0000000..2658c6f --- /dev/null +++ b/docs/src/components/ReleaseBadge.astro @@ -0,0 +1,9 @@ +--- +import { Badge } from "@astrojs/starlight/components"; + +const releaseTag = import.meta.env.PUBLIC_JPD_VERSION ?? "v4.0.0"; +const version = releaseTag.startsWith("v") ? releaseTag.slice(1) : releaseTag; +const badgeText = `Documentation for jpd ${version}`; +--- + + diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx index 37667a3..25a1e32 100644 --- a/docs/src/content/docs/index.mdx +++ b/docs/src/content/docs/index.mdx @@ -17,10 +17,11 @@ hero: icon: external --- -import { Card, CardGrid, Badge } from '@astrojs/starlight/components'; +import { Card, CardGrid } from '@astrojs/starlight/components'; +import ReleaseBadge from '../../components/ReleaseBadge.astro';
- +
## What is jpd? From edc172a22ce93c7f5441e4116493bae6f51de356 Mon Sep 17 00:00:00 2001 From: louiss0 Date: Thu, 21 May 2026 17:12:41 -0400 Subject: [PATCH 04/25] fix(release): automate Render docs version updates --- .github/workflows/ci.yml | 8 ++- .github/workflows/release.yml | 18 +++++- cmd/release_render_env/main.go | 48 ++++++++++++++ cmd/release_render_env/main_test.go | 28 +++++++++ goreleaser.yaml | 4 ++ internal/render_env/pkg.go | 86 ++++++++++++++++++++++++++ internal/render_env/render_env_test.go | 76 +++++++++++++++++++++++ scripts/cleanup.sh | 39 ------------ 8 files changed, 265 insertions(+), 42 deletions(-) create mode 100644 cmd/release_render_env/main.go create mode 100644 cmd/release_render_env/main_test.go create mode 100644 internal/render_env/pkg.go create mode 100644 internal/render_env/render_env_test.go delete mode 100755 scripts/cleanup.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd9da96..8f3a033 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -147,7 +147,13 @@ jobs: - name: Cleanup test artifacts run: | - bash ./scripts/cleanup.sh || true + set +e + [ -f javascript-package-delegator ] && rm -f javascript-package-delegator + [ -f jpd ] && rm -f jpd + [ -f coverage.out ] && rm -f coverage.out + [ -f coverage.html ] && rm -f coverage.html + find . -name "*.test" -type f -delete 2>/dev/null || true + go clean -testcache docs: name: Build docs (Astro) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a508fc3..7cb999f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,6 @@ permissions: env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - PUBLIC_JPD_VERSION: ${{ github.ref_name }} jobs: release: @@ -56,6 +55,19 @@ jobs: which nix which nix-prefetch-url || true + - name: Configure 1Password + uses: 1password/load-secrets-action/configure@v2 + with: + service-account-token: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} + + - name: Load release secrets + uses: 1password/load-secrets-action@v2 + with: + export-env: true + env: + GITHUB_TOKEN: op://Development/GORELEASER_PACKAGE_REPO_TOKEN/credential + RENDER_CLI_PACKAGES_API_KEY: op://Development/RENDER_CLI_PACKAGES_API_KEY/credential + - name: Write GoReleaser deploy keys env: GORELEASER_WINGET_DEPLOY_KEY_B64: ${{ secrets.GORELEASER_WINGET_DEPLOY_KEY_B64 }} @@ -96,4 +108,6 @@ jobs: version: "~> v2" args: release --clean --config goreleaser.yaml env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DOCS_SERVICE_ID: ${{ secrets.DOCS_SERVICE_ID }} + GITHUB_TOKEN: ${{ env.GITHUB_TOKEN }} + RENDER_CLI_PACKAGES_API_KEY: ${{ env.RENDER_CLI_PACKAGES_API_KEY }} diff --git a/cmd/release_render_env/main.go b/cmd/release_render_env/main.go new file mode 100644 index 0000000..ce66c19 --- /dev/null +++ b/cmd/release_render_env/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "context" + "flag" + "fmt" + "io" + "os" + + "github.com/louiss0/javascript-package-delegator/internal/render_env" +) + +func main() { + if err := run(os.Args[1:]); err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(1) + } +} + +func run(args []string) error { + flags := flag.NewFlagSet("release_render_env", flag.ContinueOnError) + flags.SetOutput(io.Discard) + + serviceID := flags.String("service-id", "", "Render service ID") + token := flags.String("token", "", "Render API token") + version := flags.String("version", "", "Release version to publish") + baseURL := flags.String("base-url", "https://api.render.com", "Render API base URL") + + if err := flags.Parse(args); err != nil { + return fmt.Errorf("parse release render env arguments: %w", err) + } + if *serviceID == "" { + return fmt.Errorf("service-id is required") + } + if *token == "" { + return fmt.Errorf("token is required") + } + if *version == "" { + return fmt.Errorf("version is required") + } + + updater := render_env.NewUpdater(nil, *baseURL) + if err := updater.UpdatePublicVersion(context.Background(), *serviceID, *token, *version); err != nil { + return err + } + + return nil +} diff --git a/cmd/release_render_env/main_test.go b/cmd/release_render_env/main_test.go new file mode 100644 index 0000000..a18ec9f --- /dev/null +++ b/cmd/release_render_env/main_test.go @@ -0,0 +1,28 @@ +package main + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/stretchr/testify/assert" +) + +func TestReleaseRenderEnv(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Release Render Env Suite") +} + +var _ = Describe("run", func() { + var assertT *assert.Assertions + + BeforeEach(func() { + assertT = assert.New(GinkgoT()) + }) + + It("rejects missing required arguments", func() { + err := run([]string{}) + assertT.Error(err) + assertT.Contains(err.Error(), "service-id") + }) +}) diff --git a/goreleaser.yaml b/goreleaser.yaml index afba844..e032c05 100644 --- a/goreleaser.yaml +++ b/goreleaser.yaml @@ -20,6 +20,10 @@ builds: - -X {{ .ModulePath }}/build_info.rawGO_MODE=production - -X {{ .ModulePath }}/build_info.rawBUILD_DATE={{ .Date }} + hooks: + pre: + - go run ./cmd/release_render_env --service-id "{{ .Env.DOCS_SERVICE_ID }}" --token "{{ .Env.RENDER_CLI_PACKAGES_API_KEY }}" --version "{{ .Tag }}" + archives: - formats: [tar.gz] name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" diff --git a/internal/render_env/pkg.go b/internal/render_env/pkg.go new file mode 100644 index 0000000..104f4f4 --- /dev/null +++ b/internal/render_env/pkg.go @@ -0,0 +1,86 @@ +package render_env + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" +) + +type Updater struct { + httpClient *http.Client + baseURL string +} + +type updateRequest struct { + Value string `json:"value"` +} + +func NewUpdater(httpClient *http.Client, baseURL string) Updater { + if httpClient == nil { + httpClient = http.DefaultClient + } + + trimmedBaseURL := strings.TrimRight(baseURL, "/") + if trimmedBaseURL == "" { + trimmedBaseURL = "https://api.render.com" + } + + return Updater{ + httpClient: httpClient, + baseURL: trimmedBaseURL, + } +} + +func (updater Updater) UpdatePublicVersion(ctx context.Context, serviceID string, token string, version string) error { + if strings.TrimSpace(serviceID) == "" { + return fmt.Errorf("service ID is required") + } + if strings.TrimSpace(token) == "" { + return fmt.Errorf("token is required") + } + if strings.TrimSpace(version) == "" { + return fmt.Errorf("version is required") + } + + payload, err := json.Marshal(updateRequest{Value: version}) + if err != nil { + return fmt.Errorf("marshal render env update request: %w", err) + } + + request, err := http.NewRequestWithContext( + ctx, + http.MethodPut, + fmt.Sprintf("%s/v1/services/%s/env-vars/PUBLIC_JPD_VERSION", updater.baseURL, serviceID), + bytes.NewReader(payload), + ) + if err != nil { + return fmt.Errorf("build render env update request: %w", err) + } + + request.Header.Set("accept", "application/json") + request.Header.Set("authorization", fmt.Sprintf("Bearer %s", token)) + request.Header.Set("content-type", "application/json") + + response, err := updater.httpClient.Do(request) + if err != nil { + return fmt.Errorf("update Render PUBLIC_JPD_VERSION env var: %w", err) + } + defer func() { + _ = response.Body.Close() + }() + + if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices { + body, readErr := io.ReadAll(response.Body) + if readErr != nil { + return fmt.Errorf("read Render API error response: %w", readErr) + } + + return fmt.Errorf("render API returned status %d: %s", response.StatusCode, strings.TrimSpace(string(body))) + } + + return nil +} diff --git a/internal/render_env/render_env_test.go b/internal/render_env/render_env_test.go new file mode 100644 index 0000000..b497fb6 --- /dev/null +++ b/internal/render_env/render_env_test.go @@ -0,0 +1,76 @@ +package render_env_test + +import ( + "context" + "io" + "net/http" + "net/http/httptest" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/stretchr/testify/assert" + + "github.com/louiss0/javascript-package-delegator/internal/render_env" +) + +func TestRenderEnv(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Render Env Suite") +} + +var _ = Describe("Updater", func() { + var assertT *assert.Assertions + + BeforeEach(func() { + assertT = assert.New(GinkgoT()) + }) + + Describe("UpdatePublicVersion", func() { + It("updates the Render PUBLIC_JPD_VERSION env var", func() { + server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + assertT.Equal(http.MethodPut, request.Method) + assertT.Equal("/v1/services/srv-123/env-vars/PUBLIC_JPD_VERSION", request.URL.Path) + assertT.Equal("application/json", request.Header.Get("accept")) + assertT.Equal("application/json", request.Header.Get("content-type")) + assertT.Equal("Bearer test-token", request.Header.Get("authorization")) + + body, err := io.ReadAll(request.Body) + assertT.NoError(err) + assertT.JSONEq(`{"value":"v4.0.1"}`, string(body)) + + writer.WriteHeader(http.StatusOK) + })) + defer server.Close() + + updater := render_env.NewUpdater(server.Client(), server.URL) + + err := updater.UpdatePublicVersion(context.Background(), "srv-123", "test-token", "v4.0.1") + assertT.NoError(err) + }) + + It("returns an error when the Render API rejects the update", func() { + server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { + writer.WriteHeader(http.StatusBadRequest) + _, err := writer.Write([]byte(`{"message":"invalid version"}`)) + assertT.NoError(err) + })) + defer server.Close() + + updater := render_env.NewUpdater(server.Client(), server.URL) + + err := updater.UpdatePublicVersion(context.Background(), "srv-123", "test-token", "v4.0.1") + assertT.Error(err) + assertT.Contains(err.Error(), "render API returned status 400") + assertT.Contains(err.Error(), "invalid version") + }) + + It("returns an error when a required argument is missing", func() { + updater := render_env.NewUpdater(http.DefaultClient, "https://api.render.com") + + err := updater.UpdatePublicVersion(context.Background(), "", "test-token", "v4.0.1") + assertT.Error(err) + assertT.Contains(err.Error(), "service ID") + }) + }) +}) diff --git a/scripts/cleanup.sh b/scripts/cleanup.sh deleted file mode 100755 index f41a8ea..0000000 --- a/scripts/cleanup.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash - -# Script to clean up test artifacts and accidentally generated binaries -# This should be run after tests to ensure no binary artifacts remain - -set -euo pipefail - -PROJECT_ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." &> /dev/null && pwd) -cd "$PROJECT_ROOT" - -echo "Cleaning up test artifacts and binaries..." - -# Remove the main binary if it exists (generated during tests) -if [[ -f "javascript-package-delegator" ]]; then - echo "Removing javascript-package-delegator binary..." - rm -f "javascript-package-delegator" -fi - -# Remove any test binaries -if [[ -f "jpd" ]]; then - echo "Removing jpd binary..." - rm -f "jpd" -fi - -# Remove coverage files if they exist -if [[ -f "coverage.out" ]]; then - echo "Removing coverage.out..." - rm -f "coverage.out" -fi - -if [[ -f "coverage.html" ]]; then - echo "Removing coverage.html..." - rm -f "coverage.html" -fi - -# Remove any .test files (test binaries) -find . -name "*.test" -type f -delete 2>/dev/null || true - -echo "✅ Cleanup complete" From 6f1b13f5b00ea918a1252edd8d418d000520efa3 Mon Sep 17 00:00:00 2001 From: louiss0 Date: Thu, 21 May 2026 18:15:18 -0400 Subject: [PATCH 05/25] fix(release): run Render env hook once --- cmd/release_render_env/main.go | 8 ++++---- cmd/release_render_env/main_test.go | 8 ++++++++ goreleaser.yaml | 8 ++++---- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/cmd/release_render_env/main.go b/cmd/release_render_env/main.go index ce66c19..efcea5b 100644 --- a/cmd/release_render_env/main.go +++ b/cmd/release_render_env/main.go @@ -22,7 +22,6 @@ func run(args []string) error { flags.SetOutput(io.Discard) serviceID := flags.String("service-id", "", "Render service ID") - token := flags.String("token", "", "Render API token") version := flags.String("version", "", "Release version to publish") baseURL := flags.String("base-url", "https://api.render.com", "Render API base URL") @@ -32,15 +31,16 @@ func run(args []string) error { if *serviceID == "" { return fmt.Errorf("service-id is required") } - if *token == "" { - return fmt.Errorf("token is required") + token := os.Getenv("RENDER_CLI_PACKAGES_API_KEY") + if token == "" { + return fmt.Errorf("RENDER_CLI_PACKAGES_API_KEY is required") } if *version == "" { return fmt.Errorf("version is required") } updater := render_env.NewUpdater(nil, *baseURL) - if err := updater.UpdatePublicVersion(context.Background(), *serviceID, *token, *version); err != nil { + if err := updater.UpdatePublicVersion(context.Background(), *serviceID, token, *version); err != nil { return err } diff --git a/cmd/release_render_env/main_test.go b/cmd/release_render_env/main_test.go index a18ec9f..17bf4fc 100644 --- a/cmd/release_render_env/main_test.go +++ b/cmd/release_render_env/main_test.go @@ -1,6 +1,7 @@ package main import ( + "os" "testing" . "github.com/onsi/ginkgo/v2" @@ -18,6 +19,7 @@ var _ = Describe("run", func() { BeforeEach(func() { assertT = assert.New(GinkgoT()) + assertT.NoError(os.Unsetenv("RENDER_CLI_PACKAGES_API_KEY")) }) It("rejects missing required arguments", func() { @@ -25,4 +27,10 @@ var _ = Describe("run", func() { assertT.Error(err) assertT.Contains(err.Error(), "service-id") }) + + It("rejects a missing Render API token env var", func() { + err := run([]string{"--service-id", "srv-123", "--version", "v4.0.1"}) + assertT.Error(err) + assertT.Contains(err.Error(), "RENDER_CLI_PACKAGES_API_KEY") + }) }) diff --git a/goreleaser.yaml b/goreleaser.yaml index e032c05..22862ed 100644 --- a/goreleaser.yaml +++ b/goreleaser.yaml @@ -3,6 +3,10 @@ version: 2 project_name: javascript-package-delegator +before: + hooks: + - go run ./cmd/release_render_env --service-id "{{ .Env.DOCS_SERVICE_ID }}" --version "{{ .Tag }}" + builds: - main: ./main.go id: jpd @@ -20,10 +24,6 @@ builds: - -X {{ .ModulePath }}/build_info.rawGO_MODE=production - -X {{ .ModulePath }}/build_info.rawBUILD_DATE={{ .Date }} - hooks: - pre: - - go run ./cmd/release_render_env --service-id "{{ .Env.DOCS_SERVICE_ID }}" --token "{{ .Env.RENDER_CLI_PACKAGES_API_KEY }}" --version "{{ .Tag }}" - archives: - formats: [tar.gz] name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}" From 98c23b220248b2ffb7b30d03d1f29f00289ec394 Mon Sep 17 00:00:00 2001 From: louiss0 Date: Thu, 21 May 2026 18:38:57 -0400 Subject: [PATCH 06/25] fix(release): align local Render hook updates --- .github/workflows/release.yml | 3 +-- cmd/release_render_env/main.go | 10 +++++----- goreleaser.yaml | 2 +- internal/render_env/pkg.go | 6 ++---- 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7cb999f..a45a3a0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -65,7 +65,6 @@ jobs: with: export-env: true env: - GITHUB_TOKEN: op://Development/GORELEASER_PACKAGE_REPO_TOKEN/credential RENDER_CLI_PACKAGES_API_KEY: op://Development/RENDER_CLI_PACKAGES_API_KEY/credential - name: Write GoReleaser deploy keys @@ -109,5 +108,5 @@ jobs: args: release --clean --config goreleaser.yaml env: DOCS_SERVICE_ID: ${{ secrets.DOCS_SERVICE_ID }} - GITHUB_TOKEN: ${{ env.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} RENDER_CLI_PACKAGES_API_KEY: ${{ env.RENDER_CLI_PACKAGES_API_KEY }} diff --git a/cmd/release_render_env/main.go b/cmd/release_render_env/main.go index efcea5b..e31ebfd 100644 --- a/cmd/release_render_env/main.go +++ b/cmd/release_render_env/main.go @@ -21,15 +21,15 @@ func run(args []string) error { flags := flag.NewFlagSet("release_render_env", flag.ContinueOnError) flags.SetOutput(io.Discard) - serviceID := flags.String("service-id", "", "Render service ID") + serviceID := os.Getenv("DOCS_SERVICE_ID") version := flags.String("version", "", "Release version to publish") baseURL := flags.String("base-url", "https://api.render.com", "Render API base URL") if err := flags.Parse(args); err != nil { return fmt.Errorf("parse release render env arguments: %w", err) } - if *serviceID == "" { - return fmt.Errorf("service-id is required") + if serviceID == "" { + return fmt.Errorf("DOCS_SERVICE_ID is required") } token := os.Getenv("RENDER_CLI_PACKAGES_API_KEY") if token == "" { @@ -39,8 +39,8 @@ func run(args []string) error { return fmt.Errorf("version is required") } - updater := render_env.NewUpdater(nil, *baseURL) - if err := updater.UpdatePublicVersion(context.Background(), *serviceID, token, *version); err != nil { + updater := render_env.NewUpdater(*baseURL) + if err := updater.UpdatePublicVersion(context.Background(), serviceID, token, *version); err != nil { return err } diff --git a/goreleaser.yaml b/goreleaser.yaml index 22862ed..19dcf67 100644 --- a/goreleaser.yaml +++ b/goreleaser.yaml @@ -5,7 +5,7 @@ project_name: javascript-package-delegator before: hooks: - - go run ./cmd/release_render_env --service-id "{{ .Env.DOCS_SERVICE_ID }}" --version "{{ .Tag }}" + - go run ./cmd/release_render_env --version "{{ .Tag }}" builds: - main: ./main.go diff --git a/internal/render_env/pkg.go b/internal/render_env/pkg.go index 104f4f4..5e0ce51 100644 --- a/internal/render_env/pkg.go +++ b/internal/render_env/pkg.go @@ -19,10 +19,8 @@ type updateRequest struct { Value string `json:"value"` } -func NewUpdater(httpClient *http.Client, baseURL string) Updater { - if httpClient == nil { - httpClient = http.DefaultClient - } +func NewUpdater(baseURL string) Updater { + httpClient := http.DefaultClient trimmedBaseURL := strings.TrimRight(baseURL, "/") if trimmedBaseURL == "" { From 2274b316f2ddc0062a662cc9cdcff9daf85ce0a0 Mon Sep 17 00:00:00 2001 From: louiss0 Date: Thu, 21 May 2026 18:39:54 -0400 Subject: [PATCH 07/25] ci(workflows): drop Node 24 force override --- .github/workflows/ci.yml | 24 ++++++++++++------------ .github/workflows/release.yml | 3 --- 2 files changed, 12 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f3a033..f46c737 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,12 +25,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version-file: go.mod cache: true @@ -75,11 +75,11 @@ jobs: exit 1 fi - - name: golangci-lint - uses: golangci/golangci-lint-action@v8 - with: - version: latest - args: --timeout=5m --output.text.colors=true --output.text.print-linter-name=true + - name: Install golangci-lint + run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + + - name: Run golangci-lint + run: golangci-lint run --timeout=5m --output.text.colors=true --output.text.print-linter-name=true test: name: Test (Ginkgo) + coverage @@ -91,12 +91,12 @@ jobs: os: [ubuntu-latest, windows-latest, macos-latest] steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version-file: go.mod cache: true @@ -165,7 +165,7 @@ jobs: working-directory: docs steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 with: fetch-depth: 0 @@ -211,10 +211,10 @@ jobs: goarch: [amd64, arm64] steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version-file: go.mod cache: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a45a3a0..97e0704 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,9 +10,6 @@ permissions: contents: write packages: write -env: - FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true - jobs: release: runs-on: ubuntu-latest From b78df0866fd4a315f8b1b9fd07f593c2efe29b4d Mon Sep 17 00:00:00 2001 From: louiss0 Date: Thu, 21 May 2026 18:42:34 -0400 Subject: [PATCH 08/25] ci(workflows): remove docs job from CI --- .github/workflows/ci.yml | 45 ---------------------------------------- 1 file changed, 45 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f46c737..de8eed6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -155,51 +155,6 @@ jobs: find . -name "*.test" -type f -delete 2>/dev/null || true go clean -testcache - docs: - name: Build docs (Astro) - runs-on: ubuntu-latest - needs: [lint] - defaults: - run: - shell: bash - working-directory: docs - steps: - - name: Checkout - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: 9 - run_install: false - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: "20" - cache: "pnpm" - cache-dependency-path: "docs/pnpm-lock.yaml" - - - name: Verify pnpm availability - run: pnpm --version - - - name: Install patch-package CLI (Rollup postinstall dependency) - run: npm install -g patch-package@8 - - - name: Install deps - run: pnpm install --frozen-lockfile - - - name: Build docs - run: pnpm build - - - name: Upload docs artifact - uses: actions/upload-artifact@v4 - with: - name: docs-dist - path: docs/dist - build: name: Cross-compile smoke builds runs-on: ubuntu-latest From 25cc0f3157e567a857f17f104d1344eac86ce602 Mon Sep 17 00:00:00 2001 From: louiss0 Date: Thu, 21 May 2026 19:07:57 -0400 Subject: [PATCH 09/25] feat(cmd): add integrate carapace command --- cmd/carapace.go | 110 ++++++++++++++++++++++++ cmd/completion.go | 26 +----- cmd/completion_integrate_ginkgo_test.go | 56 +++++++++++- cmd/integrate.go | 5 +- 4 files changed, 170 insertions(+), 27 deletions(-) create mode 100644 cmd/carapace.go diff --git a/cmd/carapace.go b/cmd/carapace.go new file mode 100644 index 0000000..f00b1e0 --- /dev/null +++ b/cmd/carapace.go @@ -0,0 +1,110 @@ +package cmd + +import ( + "fmt" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/rsteube/carapace" + "github.com/spf13/cobra" + + "github.com/louiss0/javascript-package-delegator/custom_errors" + "github.com/louiss0/javascript-package-delegator/custom_flags" +) + +// NewIntegrateCarapaceCmd creates the integrate carapace command. +func NewIntegrateCarapaceCmd() *cobra.Command { + outputFileFlag := custom_flags.NewFilePathFlag("output") + + carapaceCmd := &cobra.Command{ + Use: "carapace ", + Short: "Generate Carapace integration scripts", + Long: `Generate Carapace integration scripts for jpd. + +Supported shells: + bash, zsh, fish, powershell, nushell + +Examples: + jpd integrate carapace bash # Print a Carapace bash snippet to stdout + jpd integrate carapace nushell --output carapace.nu # Save Nushell Carapace setup to a file +`, + DisableFlagsInUseLine: true, + Args: func(cmd *cobra.Command, args []string) error { + return validateCompletionShell(args) + }, + RunE: func(cmd *cobra.Command, args []string) error { + return generateCarapaceSnippet(cmd, args[0], outputFileFlag.String()) + }, + } + + carapaceCmd.Flags().VarP(outputFileFlag, "output", "o", "Write Carapace script to a file instead of stdout") + + carapace.Gen(carapaceCmd).PositionalCompletion( + carapace.ActionValues(getSupportedShells()...), + ) + + return carapaceCmd +} + +func validateCompletionShell(args []string) error { + supportedShells := getSupportedShells() + sort.Strings(supportedShells) + supportedShellList := strings.Join(supportedShells, ", ") + + if len(args) != 1 { + return custom_errors.CreateInvalidArgumentErrorWithMessage( + fmt.Sprintf("requires exactly one argument representing the shell. Supported shells are: %s", supportedShellList)) + } + + shell := args[0] + idx := sort.SearchStrings(supportedShells, shell) + if idx >= len(supportedShells) || supportedShells[idx] != shell { + return custom_errors.CreateInvalidArgumentErrorWithMessage( + fmt.Sprintf("unsupported shell: '%s'. Supported shells are: %s", shell, supportedShellList)) + } + + return nil +} + +func generateCarapaceSnippet(cmd *cobra.Command, shell string, filename string) error { + snippet, err := carapace.Gen(cmd.Root()).Snippet(shell) + if err != nil { + return fmt.Errorf("failed to generate Carapace snippet for %s: %w", shell, err) + } + + if shell == "nushell" { + snippet = fmt.Sprintf("%s\n\n%s", snippet, buildNushellCarapaceSetup(cmd.Root().Name())) + } + + return writeCompletionOutput(cmd, filename, snippet) +} + +func buildNushellCarapaceSetup(commandName string) string { + return fmt.Sprintf(`$env.config = ( + $env.config | upsert completions.external { + enable: true + max_results: 100 + completer: %s_completer + } +)`, commandName) +} + +func writeCompletionOutput(cmd *cobra.Command, filename string, content string) error { + if filename == "" { + _, err := fmt.Fprint(cmd.OutOrStdout(), content) + return err + } + + dir := filepath.Dir(filename) + if err := os.MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("failed to create directory %s: %w", dir, err) + } + + if err := os.WriteFile(filename, []byte(content), 0644); err != nil { + return fmt.Errorf("failed to write output file %s: %w", filename, err) + } + + return nil +} diff --git a/cmd/completion.go b/cmd/completion.go index fe0b63a..08d347d 100644 --- a/cmd/completion.go +++ b/cmd/completion.go @@ -6,14 +6,11 @@ import ( "io" "os" "path/filepath" - "sort" - "strings" // external "github.com/spf13/cobra" // internal - "github.com/louiss0/javascript-package-delegator/custom_errors" "github.com/louiss0/javascript-package-delegator/custom_flags" integrations "github.com/louiss0/javascript-package-delegator/internal" ) @@ -85,28 +82,7 @@ Examples: `, DisableFlagsInUseLine: true, // Don't show global flags for completion command itself Args: func(cmd *cobra.Command, args []string) error { - supportedShells := getSupportedShells() - - // Sort for consistent output and efficient lookup - sort.Strings(supportedShells) - - // Generate a comma-separated list of supported shells using strings.Join - supportedShellList := strings.Join(supportedShells, ", ") - - if len(args) != 1 { - return custom_errors.CreateInvalidArgumentErrorWithMessage( - fmt.Sprintf("requires exactly one argument representing the shell. Supported shells are: %s", supportedShellList)) - } - - shell := args[0] - - // Check if the shell is supported using binary search (declarative and efficient) - idx := sort.SearchStrings(supportedShells, shell) - if idx >= len(supportedShells) || supportedShells[idx] != shell { - return custom_errors.CreateInvalidArgumentErrorWithMessage( - fmt.Sprintf("unsupported shell: '%s'. Supported shells are: %s", shell, supportedShellList)) - } - return nil + return validateCompletionShell(args) }, RunE: func(cmd *cobra.Command, args []string) error { shell := args[0] diff --git a/cmd/completion_integrate_ginkgo_test.go b/cmd/completion_integrate_ginkgo_test.go index 2d758d8..6399fc6 100644 --- a/cmd/completion_integrate_ginkgo_test.go +++ b/cmd/completion_integrate_ginkgo_test.go @@ -224,6 +224,60 @@ var _ = Describe("Completion (Ginkgo conversion)", func() { }) }) +var _ = Describe("Integrate carapace command (Ginkgo conversion)", func() { + It("shows help for the carapace command", func() { + mockRunner := mock.NewMockCommandRunner() + factory := testutil.NewRootCommandFactory(mockRunner) + factory.SetupBasicCommandRunnerExpectations() + factory.ResetDebugExecutor() + testutil.DebugExecutorExpectationManager.DebugExecutor = factory.DebugExecutor() + factory.SetupBasicDebugExecutorExpectations() + + root := factory.CreateNpmAsDefault(nil) + testutil.DebugExecutorExpectationManager.ExpectCommonPMDetectionFlow(detect.NPM, detect.PACKAGE_LOCK_JSON) + + out, err := executeCmd(root, "integrate", "carapace", "--help") + assert.NoError(GinkgoT(), err) + assert.Contains(GinkgoT(), out, "Generate Carapace integration scripts") + assert.NotContains(GinkgoT(), out, "_carapace") + }) + + It("generates a bash Carapace snippet", func() { + mockRunner := mock.NewMockCommandRunner() + factory := testutil.NewRootCommandFactory(mockRunner) + factory.SetupBasicCommandRunnerExpectations() + factory.ResetDebugExecutor() + testutil.DebugExecutorExpectationManager.DebugExecutor = factory.DebugExecutor() + factory.SetupBasicDebugExecutorExpectations() + + root := factory.CreateNpmAsDefault(nil) + testutil.DebugExecutorExpectationManager.ExpectCommonPMDetectionFlow(detect.NPM, detect.PACKAGE_LOCK_JSON) + + out, err := executeCmd(root, "integrate", "carapace", "bash") + assert.NoError(GinkgoT(), err) + assert.Contains(GinkgoT(), out, "#!/bin/bash") + assert.Contains(GinkgoT(), out, "_carapace bash") + }) + + It("appends Nushell completer registration", func() { + mockRunner := mock.NewMockCommandRunner() + factory := testutil.NewRootCommandFactory(mockRunner) + factory.SetupBasicCommandRunnerExpectations() + factory.ResetDebugExecutor() + testutil.DebugExecutorExpectationManager.DebugExecutor = factory.DebugExecutor() + factory.SetupBasicDebugExecutorExpectations() + + root := factory.CreateNpmAsDefault(nil) + testutil.DebugExecutorExpectationManager.ExpectCommonPMDetectionFlow(detect.NPM, detect.PACKAGE_LOCK_JSON) + + out, err := executeCmd(root, "integrate", "carapace", "nushell") + assert.NoError(GinkgoT(), err) + assert.Contains(GinkgoT(), out, "let jpd_completer = {|spans|") + assert.Contains(GinkgoT(), out, "completer: jpd_completer") + assert.NotContains(GinkgoT(), out, "<$carapace>_completer") + }) +}) + var _ = Describe("Embedded carapace integration (Ginkgo conversion)", func() { It("integrate help lists warp and excludes legacy carapace target", func() { mockRunner := mock.NewMockCommandRunner() @@ -239,7 +293,7 @@ var _ = Describe("Embedded carapace integration (Ginkgo conversion)", func() { out, err := executeCmd(root, "integrate", "--help") assert.NoError(GinkgoT(), err) assert.Contains(GinkgoT(), out, "warp") - assert.NotContains(GinkgoT(), out, "carapace") + assert.Contains(GinkgoT(), out, "carapace") }) It("generates a carapace shell snippet via hidden _carapace command", func() { diff --git a/cmd/integrate.go b/cmd/integrate.go index 6690bcc..2ac4986 100644 --- a/cmd/integrate.go +++ b/cmd/integrate.go @@ -21,14 +21,17 @@ func NewIntegrateCmd() *cobra.Command { Long: `Generate integration files for external tools like Warp terminal. Available integrations: - warp - Generate Warp terminal workflow files + carapace - Generate Carapace integration scripts + warp - Generate Warp terminal workflow files Examples: + jpd integrate carapace bash # Print a Carapace bash snippet jpd integrate warp # Install workflows to default directory jpd integrate warp --output-dir ./workflows # Install workflows to custom directory `, DisableFlagsInUseLine: true, } + integrateCmd.AddCommand(NewIntegrateCarapaceCmd()) integrateCmd.AddCommand(NewIntegrateWarpCmd()) return integrateCmd From b1fdfe2860e7a076dcea9276cfd690c39dc59a98 Mon Sep 17 00:00:00 2001 From: louiss0 Date: Thu, 21 May 2026 19:10:37 -0400 Subject: [PATCH 10/25] refactor(cmd): alias check using slices.Contains --- cmd/cmd_test.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go index 3e3c41d..e5b7525 100644 --- a/cmd/cmd_test.go +++ b/cmd/cmd_test.go @@ -9,6 +9,7 @@ import ( "path/filepath" "regexp" "runtime" + "slices" "strings" . "github.com/onsi/ginkgo/v2" @@ -4731,13 +4732,7 @@ var _ = Describe("JPD Commands", func() { actualAliases := dlxCmd.Aliases // Should contain "x" - found := false - for _, alias := range actualAliases { - if alias == "x" { - found = true - break - } - } + found := slices.Contains(actualAliases, "x") assert.True(found, "NewDlxCmd().Aliases should contain 'x', but got %v", actualAliases) }) }) From d055873de9712fc333c490936e644e37173fd6e0 Mon Sep 17 00:00:00 2001 From: louiss0 Date: Thu, 21 May 2026 19:12:49 -0400 Subject: [PATCH 11/25] chore: ingore env file! --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f8817fe..40eb18b 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,7 @@ forks/ vendor/ jpd javascript-package-delegator - +.env* # Test artifacts *.tmp *.log From e1c845185675f7e860f37ff640e879c3f1f2c9db Mon Sep 17 00:00:00 2001 From: louiss0 Date: Thu, 21 May 2026 19:21:18 -0400 Subject: [PATCH 12/25] docs(completion): guide Nushell users to Carapace setup --- README.adoc | 8 +++-- cmd/carapace.go | 12 +++++++ cmd/completion_integrate_ginkgo_test.go | 1 + docs/src/content/docs/how-to/completion.mdx | 31 ++++++++++++------- docs/src/content/docs/index.mdx | 4 +-- docs/src/content/docs/reference/commands.mdx | 16 +++++++--- .../content/docs/tutorial/getting-started.mdx | 4 +-- 7 files changed, 54 insertions(+), 22 deletions(-) diff --git a/README.adoc b/README.adoc index 0c7c4ee..6e9213d 100644 --- a/README.adoc +++ b/README.adoc @@ -162,7 +162,7 @@ A compact overview of `jpd` commands. For detailed usage, flags, and examples, s .Integrations (see docs for full details): * `jpd integrate warp --output-dir ./workflows/` - Generate Warp workflow files * `jpd integrate warp` - Print workflows as multi-doc YAML -* `jpd _carapace bash` - Print embedded Carapace completion bridge +* `jpd integrate carapace nushell --output ~/.config/nushell/completions/jpd.nu` - Generate Nushell Carapace setup [[integrations]] === Integrations @@ -174,12 +174,14 @@ Generate external tool integrations with the `integrate` command: # Warp workflows jpd integrate warp --output-dir ./workflows/ jpd integrate warp # print multi-doc YAML to stdout -# Carapace completion bridge (hidden command) -jpd _carapace bash +# Carapace setup for Nushell users +jpd integrate carapace nushell --output ~/.config/nushell/completions/jpd.nu ---- For flags, precedence rules, and OS paths, see the https://louiss0.github.io/javascript-package-delegator/reference/commands/#integrate[Integrate command reference]. +For developers maintaining shell setup docs: prefer `jpd integrate carapace nushell` for Nushell users, write the file to `~/.config/nushell/completions/`, and tell users to source it from `env.nu`. + == Configuring automatic installation of JavaScript package managers If a detected JavaScript package manager is not found a suitable installation command will be asked from you in an interative prompt. Once that happens the value will be stored in a file diff --git a/cmd/carapace.go b/cmd/carapace.go index f00b1e0..3eaf837 100644 --- a/cmd/carapace.go +++ b/cmd/carapace.go @@ -74,6 +74,8 @@ func generateCarapaceSnippet(cmd *cobra.Command, shell string, filename string) return fmt.Errorf("failed to generate Carapace snippet for %s: %w", shell, err) } + snippet = normalizeCarapaceExecutable(snippet, cmd.Root().Name()) + if shell == "nushell" { snippet = fmt.Sprintf("%s\n\n%s", snippet, buildNushellCarapaceSetup(cmd.Root().Name())) } @@ -81,6 +83,16 @@ func generateCarapaceSnippet(cmd *cobra.Command, shell string, filename string) return writeCompletionOutput(cmd, filename, snippet) } +func normalizeCarapaceExecutable(snippet string, commandName string) string { + executable, err := os.Executable() + if err != nil { + return snippet + } + + normalizedSnippet := strings.ReplaceAll(snippet, executable, commandName) + return strings.ReplaceAll(normalizedSnippet, filepath.Base(executable), commandName) +} + func buildNushellCarapaceSetup(commandName string) string { return fmt.Sprintf(`$env.config = ( $env.config | upsert completions.external { diff --git a/cmd/completion_integrate_ginkgo_test.go b/cmd/completion_integrate_ginkgo_test.go index 6399fc6..e40bc76 100644 --- a/cmd/completion_integrate_ginkgo_test.go +++ b/cmd/completion_integrate_ginkgo_test.go @@ -273,6 +273,7 @@ var _ = Describe("Integrate carapace command (Ginkgo conversion)", func() { out, err := executeCmd(root, "integrate", "carapace", "nushell") assert.NoError(GinkgoT(), err) assert.Contains(GinkgoT(), out, "let jpd_completer = {|spans|") + assert.Contains(GinkgoT(), out, "jpd _carapace nushell ...$spans | from json") assert.Contains(GinkgoT(), out, "completer: jpd_completer") assert.NotContains(GinkgoT(), out, "<$carapace>_completer") }) diff --git a/docs/src/content/docs/how-to/completion.mdx b/docs/src/content/docs/how-to/completion.mdx index 8f7279e..0de68c5 100644 --- a/docs/src/content/docs/how-to/completion.mdx +++ b/docs/src/content/docs/how-to/completion.mdx @@ -186,20 +186,29 @@ fish -c "complete -f" ### Nushell -You can write completion output directly to a file using the `--output` flag (supported for all shells): +For Nushell, prefer the Carapace integration command. Write the generated script to Nushell's completions folder so it can be sourced from your shell config: ```bash -# Generate completion with output flag -jpd completion nushell --output ~/.config/nushell/completions/jpd_completions.nu +# Generate the Nushell Carapace setup into the completions folder +jpd integrate carapace nushell --output ~/.config/nushell/completions/jpd.nu +``` + +Then enable the generated completer from `env.nu`: -# Add to config.nu or env.nu -echo 'source ~/.config/nushell/completions/jpd_completions.nu' >> ~/.config/nushell/config.nu +```nu +source ~/.config/nushell/completions/jpd.nu -# Restart Nushell +$env.config = ( + $env.config | upsert completions.external { + enable: true + max_results: 100 + completer: jpd_completer + } +) ``` ## Testing Completion @@ -395,12 +404,12 @@ jpd completions are: For enhanced completions across multiple shells, jpd supports [Carapace](https://carapace.sh/), a universal completion framework: ```bash -# Print the embedded Carapace completion bridge -jpd _carapace bash +# Generate Nushell Carapace setup +jpd integrate carapace nushell ``` -The hidden `_carapace` command is intended for Carapace tooling and advanced setups. -For most users, `jpd completion ` remains the primary completion flow. +For Nushell users, prefer `jpd integrate carapace nushell`. +It emits the Carapace completer and the `$env.config` registration block together. ### Warp Terminal Integration diff --git a/docs/src/content/docs/index.mdx b/docs/src/content/docs/index.mdx index 25a1e32..d0ec13d 100644 --- a/docs/src/content/docs/index.mdx +++ b/docs/src/content/docs/index.mdx @@ -94,8 +94,8 @@ jpd install # Run a script interactively jpd run -# Print the embedded Carapace completion bridge -jpd _carapace bash +# Generate Carapace setup for Nushell +jpd integrate carapace nushell # Generate Warp workflows to a folder jpd integrate warp --output-dir ./workflows/ diff --git a/docs/src/content/docs/reference/commands.mdx b/docs/src/content/docs/reference/commands.mdx index 37ea68b..6dd38d3 100644 --- a/docs/src/content/docs/reference/commands.mdx +++ b/docs/src/content/docs/reference/commands.mdx @@ -906,6 +906,7 @@ jpd integrate [flags] ### Supported Targets +- `carapace` - Generate Carapace integration scripts - `warp` - Generate Warp terminal workflow files ### Warp Integration @@ -930,20 +931,27 @@ Creates workflow files for: - `jpd-clean-install.yaml` - Clean installation - `jpd-agent.yaml` - Package manager detection -### Carapace Bridge Command +### Carapace Integration -jpd also exposes a hidden bridge command used by [Carapace](https://carapace.sh/) tooling: +Generate Carapace integration scripts with: ```bash -jpd _carapace bash +jpd integrate carapace ``` -Use this for advanced Carapace setups; most users should use `jpd completion `. +For Nushell users, prefer: + +```bash +jpd integrate carapace nushell --output ~/.config/nushell/completions/jpd.nu +``` + +This writes the Carapace completer and the Nushell `$env.config` registration block in one file. Developers should document this path for Nushell users so they can source the generated file from `env.nu` and enable Carapace once there. ### Flags | Flag | Description | Applies To | |------|-------------|------------| +| `--output` | Write Carapace script to a file instead of stdout | Carapace | | `--output-dir` | Output directory for workflow files | Warp | --- diff --git a/docs/src/content/docs/tutorial/getting-started.mdx b/docs/src/content/docs/tutorial/getting-started.mdx index c8b13e4..39fe943 100644 --- a/docs/src/content/docs/tutorial/getting-started.mdx +++ b/docs/src/content/docs/tutorial/getting-started.mdx @@ -267,8 +267,8 @@ jpd completion zsh > ~/.zsh/completions/_jpd # Fish jpd completion fish > ~/.config/fish/completions/jpd.fish -# Nushell (with output flag) -jpd completion nushell --output ~/.config/nushell/completions/jpd_completions.nu +# Nushell (recommended Carapace integration) +jpd integrate carapace nushell --output ~/.config/nushell/completions/jpd.nu ``` ## What's Next? From d1f75bda01fd2027ed6c1498a24f1bb07b30259f Mon Sep 17 00:00:00 2001 From: louiss0 Date: Fri, 22 May 2026 20:27:37 -0400 Subject: [PATCH 13/25] test(cmd): cover Carapace callback paths by OS --- cmd/carapace.go | 14 +------- cmd/completion_integrate_ginkgo_test.go | 48 +++++++++++++++++++++++-- 2 files changed, 46 insertions(+), 16 deletions(-) diff --git a/cmd/carapace.go b/cmd/carapace.go index 3eaf837..33becd0 100644 --- a/cmd/carapace.go +++ b/cmd/carapace.go @@ -74,8 +74,6 @@ func generateCarapaceSnippet(cmd *cobra.Command, shell string, filename string) return fmt.Errorf("failed to generate Carapace snippet for %s: %w", shell, err) } - snippet = normalizeCarapaceExecutable(snippet, cmd.Root().Name()) - if shell == "nushell" { snippet = fmt.Sprintf("%s\n\n%s", snippet, buildNushellCarapaceSetup(cmd.Root().Name())) } @@ -83,22 +81,12 @@ func generateCarapaceSnippet(cmd *cobra.Command, shell string, filename string) return writeCompletionOutput(cmd, filename, snippet) } -func normalizeCarapaceExecutable(snippet string, commandName string) string { - executable, err := os.Executable() - if err != nil { - return snippet - } - - normalizedSnippet := strings.ReplaceAll(snippet, executable, commandName) - return strings.ReplaceAll(normalizedSnippet, filepath.Base(executable), commandName) -} - func buildNushellCarapaceSetup(commandName string) string { return fmt.Sprintf(`$env.config = ( $env.config | upsert completions.external { enable: true max_results: 100 - completer: %s_completer + completer: $%s_completer } )`, commandName) } diff --git a/cmd/completion_integrate_ginkgo_test.go b/cmd/completion_integrate_ginkgo_test.go index e40bc76..ff9204e 100644 --- a/cmd/completion_integrate_ginkgo_test.go +++ b/cmd/completion_integrate_ginkgo_test.go @@ -3,6 +3,8 @@ package cmd_test import ( "os" "path/filepath" + "runtime" + "strings" . "github.com/onsi/ginkgo/v2" "github.com/stretchr/testify/assert" @@ -259,7 +261,41 @@ var _ = Describe("Integrate carapace command (Ginkgo conversion)", func() { assert.Contains(GinkgoT(), out, "_carapace bash") }) - It("appends Nushell completer registration", func() { + It("uses the Windows executable name for the Nushell Carapace callback", func() { + if runtime.GOOS != "windows" { + Skip("windows-specific Carapace executable assertion") + } + + mockRunner := mock.NewMockCommandRunner() + factory := testutil.NewRootCommandFactory(mockRunner) + factory.SetupBasicCommandRunnerExpectations() + factory.ResetDebugExecutor() + testutil.DebugExecutorExpectationManager.DebugExecutor = factory.DebugExecutor() + factory.SetupBasicDebugExecutorExpectations() + + root := factory.CreateNpmAsDefault(nil) + testutil.DebugExecutorExpectationManager.ExpectCommonPMDetectionFlow(detect.NPM, detect.PACKAGE_LOCK_JSON) + + out, err := executeCmd(root, "integrate", "carapace", "nushell") + assert.NoError(GinkgoT(), err) + + executable, executableErr := os.Executable() + assert.NoError(GinkgoT(), executableErr) + executableName := filepath.Base(executable) + + assert.True(GinkgoT(), strings.HasSuffix(executableName, ".exe")) + assert.Contains(GinkgoT(), out, "let jpd_completer = {|spans|") + assert.Contains(GinkgoT(), out, executableName+" _carapace nushell ...$spans | from json") + assert.NotContains(GinkgoT(), out, "jpd _carapace nushell ...$spans | from json") + assert.Contains(GinkgoT(), out, "completer: $jpd_completer") + assert.NotContains(GinkgoT(), out, "<$carapace>_completer") + }) + + It("uses the non-Windows executable name for the Nushell Carapace callback", func() { + if runtime.GOOS == "windows" { + Skip("non-Windows Carapace executable assertion") + } + mockRunner := mock.NewMockCommandRunner() factory := testutil.NewRootCommandFactory(mockRunner) factory.SetupBasicCommandRunnerExpectations() @@ -272,9 +308,15 @@ var _ = Describe("Integrate carapace command (Ginkgo conversion)", func() { out, err := executeCmd(root, "integrate", "carapace", "nushell") assert.NoError(GinkgoT(), err) + + executable, executableErr := os.Executable() + assert.NoError(GinkgoT(), executableErr) + executableName := filepath.Base(executable) + + assert.False(GinkgoT(), strings.HasSuffix(executableName, ".exe")) assert.Contains(GinkgoT(), out, "let jpd_completer = {|spans|") - assert.Contains(GinkgoT(), out, "jpd _carapace nushell ...$spans | from json") - assert.Contains(GinkgoT(), out, "completer: jpd_completer") + assert.Contains(GinkgoT(), out, executableName+" _carapace nushell ...$spans | from json") + assert.Contains(GinkgoT(), out, "completer: $jpd_completer") assert.NotContains(GinkgoT(), out, "<$carapace>_completer") }) }) From f30b4398bf29892420b56302c49e146b55482bcb Mon Sep 17 00:00:00 2001 From: louiss0 Date: Fri, 22 May 2026 20:35:14 -0400 Subject: [PATCH 14/25] docs(docs): update release badge and install guide --- docs/src/components/ReleaseBadge.astro | 2 +- docs/src/content/docs/tutorial/getting-started.mdx | 7 ++----- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/src/components/ReleaseBadge.astro b/docs/src/components/ReleaseBadge.astro index 2658c6f..3da353e 100644 --- a/docs/src/components/ReleaseBadge.astro +++ b/docs/src/components/ReleaseBadge.astro @@ -1,7 +1,7 @@ --- import { Badge } from "@astrojs/starlight/components"; -const releaseTag = import.meta.env.PUBLIC_JPD_VERSION ?? "v4.0.0"; +const releaseTag = import.meta.env.PUBLIC_JPD_VERSION ?? "dev"; const version = releaseTag.startsWith("v") ? releaseTag.slice(1) : releaseTag; const badgeText = `Documentation for jpd ${version}`; --- diff --git a/docs/src/content/docs/tutorial/getting-started.mdx b/docs/src/content/docs/tutorial/getting-started.mdx index 39fe943..faec7ce 100644 --- a/docs/src/content/docs/tutorial/getting-started.mdx +++ b/docs/src/content/docs/tutorial/getting-started.mdx @@ -27,7 +27,7 @@ jpd is distributed as a single binary and can be installed via popular package m ```bash - winget install jpd + winget install javascript-package-delegator ``` @@ -57,6 +57,7 @@ jpd is distributed as a single binary and can be installed via popular package m nix-shell -p "(import ./default.nix {}).jpd" ``` +