Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5d93d8b
fix(cmd): stabilize cwd validation test in CI
louiss0 May 21, 2026
e690a9c
fix(cmd): handle cwd test cleanup errors
louiss0 May 21, 2026
ca4a5b0
fix(release): default docs version to latest tag
louiss0 May 21, 2026
edc172a
fix(release): automate Render docs version updates
louiss0 May 21, 2026
6f1b13f
fix(release): run Render env hook once
louiss0 May 21, 2026
98c23b2
fix(release): align local Render hook updates
louiss0 May 21, 2026
2274b31
ci(workflows): drop Node 24 force override
louiss0 May 21, 2026
b78df08
ci(workflows): remove docs job from CI
louiss0 May 21, 2026
25cc0f3
feat(cmd): add integrate carapace command
louiss0 May 21, 2026
b1fdfe2
refactor(cmd): alias check using slices.Contains
louiss0 May 21, 2026
d055873
chore: ingore env file!
louiss0 May 21, 2026
e1c8451
docs(completion): guide Nushell users to Carapace setup
louiss0 May 21, 2026
d1f75bd
test(cmd): cover Carapace callback paths by OS
louiss0 May 23, 2026
f30b439
docs(docs): update release badge and install guide
louiss0 May 23, 2026
8f7f7db
fix(release): load Render env updater inputs explicitly
louiss0 May 23, 2026
93b5f43
fix(deps): support cwd-aware Deno dependency checks
louiss0 May 23, 2026
ec5d3de
refactor(mock): simplify command runner expectations
louiss0 May 23, 2026
0f1cb08
merge(main)!: resolve docs version conflicts
louiss0 May 23, 2026
da0d4ca
ci(workflows): pin lint toolchain versions
louiss0 May 23, 2026
1f98e58
ci(workflows): use compatible golangci flags
louiss0 May 23, 2026
386e533
ci(workflows): restore golangci action for v2 config
louiss0 May 23, 2026
9dc590f
docs(render-env): add package comment
louiss0 May 23, 2026
6c7e165
test(cmd): relax Carapace executable assertions
louiss0 May 23, 2026
0c54bc5
test(cmd): assert Carapace callback generically
louiss0 May 23, 2026
c95f64f
ci(go): pin CI to Go 1.26.0
louiss0 May 23, 2026
9b65171
ci(workflows): upgrade golangci for Go 1.26
louiss0 May 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 17 additions & 56 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -47,7 +47,7 @@ jobs:
fi

- name: Install goimports
run: go install golang.org/x/tools/cmd/goimports@latest
run: go install golang.org/x/tools/cmd/goimports@v0.33.0

- name: Verify goimports
run: |
Expand Down Expand Up @@ -76,10 +76,10 @@ jobs:
fi

- name: golangci-lint
uses: golangci/golangci-lint-action@v8
uses: golangci/golangci-lint-action@v9.2.1
with:
version: latest
args: --timeout=5m --output.text.colors=true --output.text.print-linter-name=true
version: v2.12.2
args: --timeout=5m

test:
name: Test (Ginkgo) + coverage
Expand All @@ -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
Expand Down Expand Up @@ -147,52 +147,13 @@ jobs:

- name: Cleanup test artifacts
run: |
bash ./scripts/cleanup.sh || true

docs:
name: Build docs (Astro)
runs-on: ubuntu-latest
needs: [lint]
defaults:
run:
shell: bash
working-directory: docs
steps:
- name: Checkout
uses: actions/checkout@v4
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
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

build:
name: Cross-compile smoke builds
Expand All @@ -205,10 +166,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
Expand Down
17 changes: 14 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,6 @@ permissions:
contents: write
packages: write

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

jobs:
release:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -55,6 +52,18 @@ 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:
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 }}
Expand Down Expand Up @@ -95,4 +104,6 @@ jobs:
version: "~> v2"
args: release --clean --config goreleaser.yaml
env:
DOCS_SERVICE_ID: ${{ secrets.DOCS_SERVICE_ID }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RENDER_CLI_PACKAGES_API_KEY: ${{ env.RENDER_CLI_PACKAGES_API_KEY }}
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ forks/
vendor/
jpd
javascript-package-delegator

.env*
# Test artifacts
*.tmp
*.log
Expand Down
8 changes: 5 additions & 3 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
110 changes: 110 additions & 0 deletions cmd/carapace.go
Original file line number Diff line number Diff line change
@@ -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 <shell>",
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
}
22 changes: 11 additions & 11 deletions cmd/cmd_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"path/filepath"
"regexp"
"runtime"
"slices"
"strings"

. "github.com/onsi/ginkgo/v2"
Expand Down Expand Up @@ -353,14 +354,19 @@ 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 func() {
assert.NoError(os.RemoveAll(tempDir))
}()

invalidPath = filepath.ToSlash(tempDir)
}

_, err := executeCmd(currentRootCmd, "--cwd", invalidPath)
Expand Down Expand Up @@ -4726,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)
})
})
Expand Down
26 changes: 1 addition & 25 deletions cmd/completion.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand Down Expand Up @@ -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]
Expand Down
Loading
Loading