diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fd9da96..dc69926 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 @@ -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: | @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a47e0e5..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 @@ -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 }} @@ -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 }} 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 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 new file mode 100644 index 0000000..33becd0 --- /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/cmd_test.go b/cmd/cmd_test.go index 3f0308e..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" @@ -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) @@ -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) }) }) 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..4583810 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" + "regexp" + "runtime" . "github.com/onsi/ginkgo/v2" "github.com/stretchr/testify/assert" @@ -224,6 +226,92 @@ 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("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) + + assert.Contains(GinkgoT(), out, "let jpd_completer = {|spans|") + assert.Regexp(GinkgoT(), regexp.MustCompile(`(?m)\s+\S+ _carapace nushell \.\.\.\$spans \| from json`), out) + 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() + 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.Regexp(GinkgoT(), regexp.MustCompile(`(?m)\s+\S+ _carapace nushell \.\.\.\$spans \| from json`), out) + assert.NotContains(GinkgoT(), out, "jpd _carapace nushell ...$spans | from json") + 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 +327,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 diff --git a/cmd/release_render_env/main.go b/cmd/release_render_env/main.go new file mode 100644 index 0000000..ac112c5 --- /dev/null +++ b/cmd/release_render_env/main.go @@ -0,0 +1,49 @@ +package main + +import ( + "context" + "flag" + "fmt" + "io" + "net/http" + "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 := 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("DOCS_SERVICE_ID 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(http.DefaultClient, *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..88c435e --- /dev/null +++ b/cmd/release_render_env/main_test.go @@ -0,0 +1,39 @@ +package main + +import ( + "os" + "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()) + assertT.NoError(os.Unsetenv("DOCS_SERVICE_ID")) + assertT.NoError(os.Unsetenv("RENDER_CLI_PACKAGES_API_KEY")) + }) + + It("rejects a missing docs service ID env var", func() { + err := run([]string{"--version", "v4.0.1"}) + assertT.Error(err) + assertT.Contains(err.Error(), "DOCS_SERVICE_ID") + }) + + It("rejects a missing Render API token env var", func() { + assertT.NoError(os.Setenv("DOCS_SERVICE_ID", "srv-123")) + + err := run([]string{"--version", "v4.0.1"}) + assertT.Error(err) + assertT.Contains(err.Error(), "RENDER_CLI_PACKAGES_API_KEY") + }) +}) diff --git a/cmd/start.go b/cmd/start.go index ca67ccd..451245a 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -266,7 +266,7 @@ func autoInstallDependenciesIfNeeded( } } } else { - importValues, err := deps.ExtractImportsFromDenoJSON() + importValues, err := deps.ExtractImportsFromDenoJSON(baseDir) if err == nil && len(importValues) > 0 { const maxImportChecks = 5 checksToRun := importValues @@ -296,7 +296,7 @@ func autoInstallDependenciesIfNeeded( currentHash, err := deps.ComputeDenoImportsHash(baseDir) if err == nil { - storedHash, err := deps.ReadStoredDepsHash(baseDir) + storedHash, err := deps.ReadStoredDenoDepsHash(baseDir) if err == nil { hashMismatch := storedHash == "" || currentHash != storedHash if hashMismatch { @@ -370,15 +370,20 @@ func autoInstallDependenciesIfNeeded( } } } else { - de.LogJSCommandIfDebugIsTrue("deno", "cache", "deno.json") - cmdRunner.Command("deno", "cache", "deno.json") + denoConfigPath, err := deps.DenoConfigPath(baseDir) + if err != nil { + return fmt.Errorf("failed to locate Deno config for caching: %w", err) + } + + de.LogJSCommandIfDebugIsTrue("deno", "cache", denoConfigPath) + cmdRunner.Command("deno", "cache", denoConfigPath) if err := cmdRunner.Run(); err != nil { if goEnv.IsDevelopmentMode() { de.LogDebugMessageIfDebugIsTrue("Deno cache failed, continuing", "error", err) } } else { if newHash, err := deps.ComputeDenoImportsHash(baseDir); err == nil { - if err := deps.WriteStoredDepsHash(baseDir, newHash); err == nil { + if err := deps.WriteStoredDenoDepsHash(baseDir, newHash); err == nil { if goEnv.IsDevelopmentMode() { hashShort := "" if len(newHash) >= 8 { diff --git a/cmd/uninstall.go b/cmd/uninstall.go index 58de71a..5ccf9ed 100644 --- a/cmd/uninstall.go +++ b/cmd/uninstall.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" + "os" "strings" "github.com/charmbracelet/huh" @@ -56,6 +57,17 @@ Examples: goEnv := getGoEnvFromCommandContext(cmd) de := getDebugExecutorFromCommandContext(cmd) + targetDir, err := cmd.Flags().GetString(_CWD_FLAG) + if err != nil { + return fmt.Errorf("failed to parse --%s flag: %w", _CWD_FLAG, err) + } + if targetDir == "" { + targetDir, err = os.Getwd() + if err != nil { + return fmt.Errorf("failed to determine working directory: %w", err) + } + } + goEnv.ExecuteIfModeIsProduction(func() { log.Info("Using package manager", "pm", pm) }) @@ -86,7 +98,7 @@ Examples: if packageIsDeno { - dependencies, err = deps.ExtractImportsFromDenoJSON() + dependencies, err = deps.ExtractImportsFromDenoJSON(targetDir) if err != nil { return err } @@ -113,50 +125,24 @@ Examples: } // Build command based on package manager and flags - var cmdArgs []string - switch pm { - case detect.NPM: - cmdArgs = []string{"uninstall"} - cmdArgs = lo.Flatten([][]string{cmdArgs, selectedPackages, args}) - - if global { - cmdArgs = append(cmdArgs, "--global") - } - - case detect.YARN: - cmdArgs = []string{"remove"} - cmdArgs = lo.Flatten([][]string{cmdArgs, selectedPackages, args}) - - if global { - cmdArgs = append(cmdArgs, "--global") - } - - case detect.PNPM: - cmdArgs = []string{"remove"} - cmdArgs = lo.Flatten([][]string{cmdArgs, selectedPackages, args}) - - if global { - cmdArgs = append(cmdArgs, "--global") - } - - case detect.BUN: - cmdArgs = []string{"remove"} - cmdArgs = lo.Flatten([][]string{cmdArgs, selectedPackages, args}) - - if global { - cmdArgs = append(cmdArgs, "--global") - } + baseArgs := lo.Switch[string, []string](pm). + Case(detect.NPM, []string{"uninstall"}). + Case(detect.YARN, []string{"remove"}). + Case(detect.PNPM, []string{"remove"}). + Case(detect.BUN, []string{"remove"}). + CaseF(detect.DENO, func() []string { + return []string{lo.Ternary[string](global, "uninstall", "remove")} + }). + Default([]string{}) + + if len(baseArgs) == 0 { + return fmt.Errorf("unsupported package manager: %s", pm) + } - case detect.DENO: - if global { - cmdArgs = []string{"uninstall"} - } else { - cmdArgs = []string{"remove"} - } - cmdArgs = lo.Flatten([][]string{cmdArgs, selectedPackages, args}) + cmdArgs := lo.Flatten([][]string{baseArgs, selectedPackages, args}) - default: - return fmt.Errorf("unsupported package manager: %s", pm) + if global && pm != detect.DENO { + cmdArgs = append(cmdArgs, "--global") } // Execute the command 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..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" ``` +