From cef8df4e4cdad673099420a903858e7199da46b4 Mon Sep 17 00:00:00 2001 From: shelton louis Date: Thu, 16 Apr 2026 03:07:30 +0000 Subject: [PATCH 1/7] feat: add dev command and simplify create command - Add new 'dev' command (jpd dev) that runs the dev script with automatic dependency checks (auto-install via autoInstallDependenciesIfNeeded) - npm/pnpm: runs 'npm run dev [-- args]' - yarn/bun: runs 'yarn run dev [args]' - deno: runs 'deno task dev [args]' - Supports --no-volta flag to disable Volta integration - Simplify 'create' command to delegate directly to each PM's own native create command, removing the complex create-runner logic: - npm: runs 'npm create ' - pnpm: runs 'pnpm create ' - yarn: runs 'yarn create ' - bun: runs 'bun create ' - deno: runs 'deno init ' - Removes BuildCreateCommand, CreateAppSelector, CreateAppSearcher, search flag (--search/-s/--size), and yarn version detection - Update root.go: register NewDevCmd(), simplify NewCreateCmd() call, remove NewCreateAppSearcher/NewCreateAppSelector from Dependencies - Update testutil/pkg.go: remove search-related mock deps - Update tests: rewrite create tests for new behavior, add dev tests Co-Authored-By: Oz --- cmd/cmd_test.go | 459 +++++++++++++++++++----------------------------- cmd/create.go | 379 +++------------------------------------ cmd/dev.go | 123 +++++++++++++ cmd/root.go | 21 +-- testutil/pkg.go | 27 --- 5 files changed, 337 insertions(+), 672 deletions(-) create mode 100644 cmd/dev.go diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go index 02102e4..2cf19be 100644 --- a/cmd/cmd_test.go +++ b/cmd/cmd_test.go @@ -1602,14 +1602,8 @@ var _ = Describe("JPD Commands", func() { It("should show help", func() { output, err := executeCmd(rootCmd, "create", "--help") assert.NoError(err) - assert.Contains(output, "Scaffold a new project using the appropriate package manager's create command") + assert.Contains(output, "Scaffold a new project by delegating to the package manager's native create command") assert.Contains(output, "jpd create") - // Create-specific flags are visible in help - assert.Contains(output, "--search") - assert.Contains(output, "-s") - assert.Contains(output, "--size") - // Guidance about npm separator normalization - assert.Contains(output, "JPD automatically inserts the -- separator") }) It("should have correct aliases", func() { @@ -1622,252 +1616,65 @@ var _ = Describe("JPD Commands", func() { assert.Contains(err.Error(), "requires at least 1 arg(s)") }) - // BuildCreateCommand Unit Tests - Describe("BuildCreateCommand function tests", func() { - Describe("npm create command", func() { - It("should build npm exec create-react-app command", func() { - program, args, err := cmd.BuildCreateCommand("npm", "", "react-app", []string{"my-app"}) - assert.NoError(err) - assert.Equal("npm", program) - assert.Equal([]string{"exec", "create-react-app", "--", "my-app"}, args) - }) - - It("should handle package already prefixed with create-", func() { - program, args, err := cmd.BuildCreateCommand("npm", "", "create-react-app", []string{"my-app"}) - assert.NoError(err) - assert.Equal("npm", program) - assert.Equal([]string{"exec", "create-react-app", "--", "my-app"}, args) - }) - - It("should handle version specifiers", func() { - program, args, err := cmd.BuildCreateCommand("npm", "", "vite@latest", []string{"my-app", "--template", "react"}) - assert.NoError(err) - assert.Equal("npm", program) - assert.Equal([]string{"exec", "create-vite@latest", "--", "my-app", "--template", "react"}, args) - }) - - It("should handle scoped packages", func() { - program, args, err := cmd.BuildCreateCommand("npm", "", "@org/starter", []string{"my-app"}) - assert.NoError(err) - assert.Equal("npm", program) - assert.Equal([]string{"exec", "@org/starter", "--", "my-app"}, args) - }) - - It("should return error when no name provided", func() { - _, _, err := cmd.BuildCreateCommand("npm", "", "", []string{}) - assert.Error(err) - assert.Contains(err.Error(), "package name is required") - }) - - It("should reject URLs", func() { - _, _, err := cmd.BuildCreateCommand("npm", "", "https://example.com/create.js", []string{}) - assert.Error(err) - assert.Contains(err.Error(), "URLs are not supported for npm") - }) - }) - - Describe("pnpm create command", func() { - It("should build pnpm exec command", func() { - program, args, err := cmd.BuildCreateCommand("pnpm", "", "react-app", []string{"my-app"}) - assert.NoError(err) - assert.Equal("pnpm", program) - assert.Equal([]string{"dlx", "create-react-app", "my-app"}, args) - }) - - It("should handle version specifiers", func() { - program, args, err := cmd.BuildCreateCommand("pnpm", "", "vite@4", []string{"my-app", "--template", "vue"}) - assert.NoError(err) - assert.Equal("pnpm", program) - assert.Equal([]string{"dlx", "create-vite@4", "my-app", "--template", "vue"}, args) - }) - }) - - Describe("yarn create command", func() { - It("should use npx for yarn v1", func() { - program, args, err := cmd.BuildCreateCommand("yarn", "1.22.0", "react-app", []string{"my-app"}) - assert.NoError(err) - assert.Equal("npx", program) - assert.Equal([]string{"create-react-app", "my-app"}, args) - }) - - It("should use yarn dlx for yarn v2+", func() { - program, args, err := cmd.BuildCreateCommand("yarn", "2.0.0", "react-app", []string{"my-app"}) - assert.NoError(err) - assert.Equal("yarn", program) - assert.Equal([]string{"dlx", "create-react-app", "my-app"}, args) - }) - - It("should use yarn dlx for yarn berry", func() { - program, args, err := cmd.BuildCreateCommand("yarn", "berry-3.1.0", "react-app", []string{"my-app"}) - assert.NoError(err) - assert.Equal("yarn", program) - assert.Equal([]string{"dlx", "create-react-app", "my-app"}, args) - }) - }) - - Describe("bun create command", func() { - It("should use bunx", func() { - program, args, err := cmd.BuildCreateCommand("bun", "", "react-app", []string{"my-app"}) - assert.NoError(err) - assert.Equal("bunx", program) - assert.Equal([]string{"create-react-app", "my-app"}, args) - }) - }) - - Describe("deno create command", func() { - It("should use deno run with valid URL", func() { - program, args, err := cmd.BuildCreateCommand("deno", "", "https://deno.land/x/fresh/init.ts", []string{"my-app"}) - assert.NoError(err) - assert.Equal("deno", program) - assert.Equal([]string{"run", "https://deno.land/x/fresh/init.ts", "my-app"}, args) - }) - - It("should reject invalid URL", func() { - _, _, err := cmd.BuildCreateCommand("deno", "", "not-a-url", []string{}) - assert.Error(err) - assert.Contains(err.Error(), "deno create requires a valid URL") - }) - - It("should reject empty URL", func() { - _, _, err := cmd.BuildCreateCommand("deno", "", "", []string{}) - assert.Error(err) - assert.Contains(err.Error(), "deno create requires a URL") - }) - }) - - Describe("error cases", func() { - It("should reject unsupported package manager", func() { - _, _, err := cmd.BuildCreateCommand("unsupported", "", "react-app", []string{}) - assert.Error(err) - assert.Contains(err.Error(), "unsupported package manager") - }) - }) - }) - Context("npm", func() { - It("should execute npm exec create-react-app", func() { + It("delegates to npm create with args", func() { DebugExecutorExpectationManager.ExpectCommonPMDetectionFlow(detect.NPM, detect.PACKAGE_LOCK_JSON) - DebugExecutorExpectationManager.ExpectJSCommandLog("npm", "exec", "create-react-app", "--", "my-app") + DebugExecutorExpectationManager.ExpectJSCommandLog("npm", "create", "react-app", "my-app") _, err := executeCmd(rootCmd, "create", "react-app", "my-app") assert.NoError(err) - assert.True(mockCommandRunner.HasCommand("npm", "exec", "create-react-app", "--", "my-app")) + assert.True(mockCommandRunner.HasCommand("npm", "create", "react-app", "my-app")) }) - It("should execute npm exec create-react-app with additional args", func() { + It("delegates to npm create with version specifier", func() { DebugExecutorExpectationManager.ExpectCommonPMDetectionFlow(detect.NPM, detect.PACKAGE_LOCK_JSON) - DebugExecutorExpectationManager.ExpectJSCommandLog("npm", "exec", "create-react-app", "--", "my-app", "--template", "typescript") - _, err := executeCmd(rootCmd, "create", "react-app", "my-app", "--template", "typescript") - assert.NoError(err) - assert.True(mockCommandRunner.HasCommand("npm", "exec", "create-react-app", "--", "my-app", "--template", "typescript")) - }) - - It("should handle create prefix already present", func() { - DebugExecutorExpectationManager.ExpectCommonPMDetectionFlow(detect.NPM, detect.PACKAGE_LOCK_JSON) - DebugExecutorExpectationManager.ExpectJSCommandLog("npm", "exec", "create-react-app", "--", "my-app") - _, err := executeCmd(rootCmd, "create", "create-react-app", "my-app") - assert.NoError(err) - assert.True(mockCommandRunner.HasCommand("npm", "exec", "create-react-app", "--", "my-app")) - }) - - It("should handle version specifiers", func() { - DebugExecutorExpectationManager.ExpectCommonPMDetectionFlow(detect.NPM, detect.PACKAGE_LOCK_JSON) - DebugExecutorExpectationManager.ExpectJSCommandLog("npm", "exec", "create-vite@latest", "--", "my-app") + DebugExecutorExpectationManager.ExpectJSCommandLog("npm", "create", "vite@latest", "my-app") _, err := executeCmd(rootCmd, "create", "vite@latest", "my-app") assert.NoError(err) - assert.True(mockCommandRunner.HasCommand("npm", "exec", "create-vite@latest", "--", "my-app")) - }) - - It("normalizes an extra user-provided -- for npm", func() { - DebugExecutorExpectationManager.ExpectCommonPMDetectionFlow(detect.NPM, detect.PACKAGE_LOCK_JSON) - // We only assert the executed command; allow any debug JS command log - DebugExecutorExpectationManager.ExpectJSCommandRandomLog() - _, err := executeCmd(rootCmd, "create", "vite@latest", "my-app", "--", "--template", "react") - assert.NoError(err) - // Ensure only a single -- is present in the executed command - assert.True(mockCommandRunner.HasCommand("npm", "exec", "create-vite@latest", "--", "my-app", "--template", "react")) + assert.True(mockCommandRunner.HasCommand("npm", "create", "vite@latest", "my-app")) }) }) Context("pnpm", func() { - var pnpmRootCmd *cobra.Command - - BeforeEach(func() { - pnpmRootCmd = factory.CreatePnpmAsDefault(nil) - }) - - It("should execute pnpm exec create-react-app", func() { - DebugExecutorExpectationManager.ExpectCommonPMDetectionFlow(detect.PNPM, detect.PNPM_LOCK_YAML) - DebugExecutorExpectationManager.ExpectJSCommandLog("pnpm", "dlx", "create-react-app", "my-app") - _, err := executeCmd(pnpmRootCmd, "create", "react-app", "my-app") - assert.NoError(err) - assert.True(mockCommandRunner.HasCommand("pnpm", "dlx", "create-react-app", "my-app")) - }) - - It("does not strip a user-provided -- for pnpm", func() { + It("delegates to pnpm create with args", func() { + pnpmRootCmd := factory.CreatePnpmAsDefault(nil) DebugExecutorExpectationManager.ExpectCommonPMDetectionFlow(detect.PNPM, detect.PNPM_LOCK_YAML) - DebugExecutorExpectationManager.ExpectJSCommandLog("pnpm", "dlx", "create-vite@latest", "my-app", "--", "--template", "react") - _, err := executeCmd(pnpmRootCmd, "create", "vite@latest", "my-app", "--", "--template", "react") + DebugExecutorExpectationManager.ExpectJSCommandLog("pnpm", "create", "vite@latest", "my-app") + _, err := executeCmd(pnpmRootCmd, "create", "vite@latest", "my-app") assert.NoError(err) - assert.True(mockCommandRunner.HasCommand("pnpm", "dlx", "create-vite@latest", "my-app", "--", "--template", "react")) + assert.True(mockCommandRunner.HasCommand("pnpm", "create", "vite@latest", "my-app")) }) }) Context("yarn", func() { - It("should execute npx create-react-app for yarn v1", func() { + It("delegates to yarn create for both v1 and v2+", func() { yarnRootCmd := factory.CreateYarnOneAsDefault(nil) DebugExecutorExpectationManager.ExpectCommonPathDetectionFlow(detect.YARN) - DebugExecutorExpectationManager.ExpectJSCommandLog("npx", "create-react-app", "my-app") - _, err := executeCmd(yarnRootCmd, "create", "react-app", "my-app") - assert.NoError(err) - assert.True(mockCommandRunner.HasCommand("npx", "create-react-app", "my-app")) - }) - - It("should execute yarn dlx create-react-app for yarn v2+", func() { - yarnRootCmd := factory.CreateYarnTwoAsDefault(nil) - DebugExecutorExpectationManager.ExpectCommonPMDetectionFlow(detect.YARN, detect.YARN_LOCK) - DebugExecutorExpectationManager.ExpectJSCommandLog("yarn", "dlx", "create-react-app", "my-app") + DebugExecutorExpectationManager.ExpectJSCommandLog("yarn", "create", "react-app", "my-app") _, err := executeCmd(yarnRootCmd, "create", "react-app", "my-app") assert.NoError(err) - assert.True(mockCommandRunner.HasCommand("yarn", "dlx", "create-react-app", "my-app")) + assert.True(mockCommandRunner.HasCommand("yarn", "create", "react-app", "my-app")) }) }) Context("bun", func() { - var bunRootCmd *cobra.Command - - BeforeEach(func() { - bunRootCmd = factory.CreateBunAsDefault(nil) - }) - - It("should execute bunx create-react-app", func() { + It("delegates to bun create with args", func() { + bunRootCmd := factory.CreateBunAsDefault(nil) DebugExecutorExpectationManager.ExpectCommonPMDetectionFlow(detect.BUN, detect.BUN_LOCKB) - DebugExecutorExpectationManager.ExpectJSCommandLog("bunx", "create-react-app", "my-app") + DebugExecutorExpectationManager.ExpectJSCommandLog("bun", "create", "react-app", "my-app") _, err := executeCmd(bunRootCmd, "create", "react-app", "my-app") assert.NoError(err) - assert.True(mockCommandRunner.HasCommand("bunx", "create-react-app", "my-app")) + assert.True(mockCommandRunner.HasCommand("bun", "create", "react-app", "my-app")) }) }) Context("deno", func() { - var denoRootCmd *cobra.Command - - BeforeEach(func() { - denoRootCmd = factory.CreateDenoAsDefault(nil) - }) - - It("should execute deno run with URL", func() { + It("delegates to deno init with args", func() { + denoRootCmd := factory.CreateDenoAsDefault(nil) DebugExecutorExpectationManager.ExpectCommonPMDetectionFlow(detect.DENO, detect.DENO_JSON) - DebugExecutorExpectationManager.ExpectJSCommandLog("deno", "run", "https://deno.land/x/fresh/init.ts", "my-app") - _, err := executeCmd(denoRootCmd, "create", "https://deno.land/x/fresh/init.ts", "my-app") + DebugExecutorExpectationManager.ExpectJSCommandLog("deno", "init", "my-deno-app") + _, err := executeCmd(denoRootCmd, "create", "my-deno-app") assert.NoError(err) - assert.True(mockCommandRunner.HasCommand("deno", "run", "https://deno.land/x/fresh/init.ts", "my-app")) - }) - - It("should reject a non-URL for create", func() { - DebugExecutorExpectationManager.ExpectCommonPMDetectionFlow(detect.DENO, detect.DENO_JSON) - _, err := executeCmd(denoRootCmd, "create", "not-a-url") - assert.Error(err) - assert.Contains(err.Error(), "deno create requires a valid URL") + assert.True(mockCommandRunner.HasCommand("deno", "init", "my-deno-app")) }) }) @@ -1875,7 +1682,6 @@ var _ = Describe("JPD Commands", func() { It("should return error for unsupported package manager", func() { rootCmd := factory.GenerateWithPackageManagerDetector("unknown", nil) DebugExecutorExpectationManager.ExpectCommonPMDetectionFlow("unknown", detect.PACKAGE_LOCK_JSON) - _, err := executeCmd(rootCmd, "create", "react-app", "my-app") assert.Error(err) assert.Contains(err.Error(), "unsupported package manager") @@ -1885,73 +1691,169 @@ var _ = Describe("JPD Commands", func() { rootCmd := factory.CreateNpmAsDefault(nil) mockCommandRunner.InvalidCommands = []string{"npm"} DebugExecutorExpectationManager.ExpectCommonPMDetectionFlow(detect.NPM, detect.PACKAGE_LOCK_JSON) - DebugExecutorExpectationManager.ExpectJSCommandLog("npm", "exec", "create-react-app", "--", "my-app") + DebugExecutorExpectationManager.ExpectJSCommandLog("npm", "create", "react-app", "my-app") _, err := executeCmd(rootCmd, "create", "react-app", "my-app") assert.Error(err) assert.Contains(err.Error(), "mock error: command 'npm' is configured to fail") }) }) - Context("Search flag", func() { - It("requires a value", func() { - _, err := executeCmd(rootCmd, "create", "--search") - assert.Error(err) - assert.Contains(err.Error(), "flag needs an argument: --search") + }) + + const DevCommand = "Dev Command" + Describe(DevCommand, func() { + + It("should show help", func() { + output, err := executeCmd(rootCmd, "dev", "--help") + assert.NoError(err) + assert.Contains(output, "Dev runs the dev script after ensuring dependencies are installed") + assert.Contains(output, "jpd dev") + }) + + It("runs npm run dev and forwards extra arguments", func() { + rootCmd := factory.CreateNpmAsDefault(nil) + testDir := GinkgoT().TempDir() + originalDir, err := os.Getwd() + assert.NoError(err) + err = os.Chdir(testDir) + assert.NoError(err) + GinkgoT().Cleanup(func() { + if originalDir != "" { + _ = os.Chdir(originalDir) + } }) - It("errors when args are passed with the search flag", func() { - _, err := executeCmd(rootCmd, "create", "some-arg", "--search", "query") - assert.Error(err) - assert.Contains(err.Error(), "when using the --search flag, you cannot pass any other arguments") - }) - - // TODO: Skipped - requires mock.NewMockHTTPClient which doesn't exist - // It("errors when the search returns no results", func() { - // // Setup expectations for npm and a failed search - // DebugExecutorExpectationManager.ExpectCommonPMDetectionFlow(detect.NPM, detect.PACKAGE_LOCK_JSON) - // - // // Configure the mock HTTP client to return an empty search result - // mockHTTPClient := mock.NewMockHTTPClient() - // mockHTTPClient.ResponseBody = `{"objects":[]}` - // services.SetDefaultClient(mockHTTPClient) - // - // _, err := executeCmd(rootCmd, "create", "--search", "nonexistent-package") - // assert.Error(err) - // assert.Contains(err.Error(), "No packages found for query") - // - // // Restore default client - // services.SetDefaultClient(nil) - // }) - // - // It("executes the selected package after a successful search", func() { - // // Setup expectations - // DebugExecutorExpectationManager.ExpectCommonPMDetectionFlow(detect.NPM, detect.PACKAGE_LOCK_JSON) - // DebugExecutorExpectationManager.ExpectJSCommandLog("npm", "exec", "create-vite", "--", "my-app") - // - // // Configure mock HTTP client for successful search - // mockHTTPClient := mock.NewMockHTTPClient() - // searchResults := services.PackageSearch{ - // Objects: []services.Object{ - // {Package: services.Package{Name: "create-vite", Version: "1.0.0"}}, - // {Package: services.Package{Name: "create-react-app", Version: "1.0.0"}}, - // }, - // } - // body, _ := json.Marshal(searchResults) - // mockHTTPClient.ResponseBody = string(body) - // services.SetDefaultClient(mockHTTPClient) - // - // // Configure the UI to select "create-vite" - // rootCmd := factory.CreateWithSelectedPackage("create-vite") - // - // _, err := executeCmd(rootCmd, "create", "--search", "vite", "my-app") - // assert.NoError(err) - // - // // Verify the correct command was run - // assert.True(mockCommandRunner.HasCommand("npm", "exec", "create-vite", "--", "my-app")) - // - // // Restore default client - // services.SetDefaultClient(nil) - // }) + err = os.WriteFile(filepath.Join(testDir, "package.json"), []byte(`{"scripts": {"dev": "echo dev"}}`), 0644) + assert.NoError(err) + err = os.WriteFile(filepath.Join(testDir, "package-lock.json"), []byte(""), 0644) + assert.NoError(err) + err = os.Mkdir(filepath.Join(testDir, "node_modules"), 0755) + assert.NoError(err) + + DebugExecutorExpectationManager.ExpectLockfileDetected(detect.PACKAGE_LOCK_JSON) + DebugExecutorExpectationManager.ExpectPMDetectedFromLockfile(detect.NPM) + _, err = executeCmd(rootCmd, "dev", "--", "--host", "0.0.0.0") + assert.NoError(err) + assert.True(mockCommandRunner.HasCommand("npm", "run", "dev", "--", "--host", "0.0.0.0")) + }) + + It("runs pnpm run dev with auto-install when node_modules missing", func() { + rootCmd := factory.CreatePnpmAsDefault(nil) + testDir := GinkgoT().TempDir() + originalDir, err := os.Getwd() + assert.NoError(err) + err = os.Chdir(testDir) + assert.NoError(err) + GinkgoT().Cleanup(func() { + if originalDir != "" { + _ = os.Chdir(originalDir) + } + }) + + err = os.WriteFile(filepath.Join(testDir, "package.json"), []byte(`{"scripts": {"dev": "echo dev"}}`), 0644) + assert.NoError(err) + err = os.WriteFile(filepath.Join(testDir, "pnpm-lock.yaml"), []byte(""), 0644) + assert.NoError(err) + + DebugExecutorExpectationManager.ExpectLockfileDetected(detect.PNPM_LOCK_YAML) + DebugExecutorExpectationManager.ExpectPMDetectedFromLockfile(detect.PNPM) + _, err = executeCmd(rootCmd, "dev") + assert.NoError(err) + + assert.True(mockCommandRunner.WasCommandCalled("pnpm", "install")) + assert.True(mockCommandRunner.HasCommand("pnpm", "run", "dev")) + }) + + It("passes through args without double dash for yarn", func() { + rootCmd := factory.CreateYarnTwoAsDefault(nil) + testDir := GinkgoT().TempDir() + originalDir, err := os.Getwd() + assert.NoError(err) + err = os.Chdir(testDir) + assert.NoError(err) + GinkgoT().Cleanup(func() { + if originalDir != "" { + _ = os.Chdir(originalDir) + } + }) + + err = os.WriteFile(filepath.Join(testDir, "package.json"), []byte(`{"scripts": {"dev": "yarn dev"}}`), 0644) + assert.NoError(err) + err = os.WriteFile(filepath.Join(testDir, "yarn.lock"), []byte(""), 0644) + assert.NoError(err) + err = os.Mkdir(filepath.Join(testDir, "node_modules"), 0755) + assert.NoError(err) + + _, err = executeCmd(rootCmd, "dev", "--", "--inspect") + assert.NoError(err) + assert.True(mockCommandRunner.HasCommand("yarn", "run", "dev", "--inspect")) + }) + + It("runs deno task dev", func() { + testDir := GinkgoT().TempDir() + originalDir, err := os.Getwd() + assert.NoError(err) + err = os.Chdir(testDir) + assert.NoError(err) + GinkgoT().Cleanup(func() { + if originalDir != "" { + _ = os.Chdir(originalDir) + } + }) + + rootCmd := factory.CreateDenoAsDefault(nil) + err = os.WriteFile(filepath.Join(testDir, "deno.json"), []byte(`{"tasks": {"dev": "deno task dev"}, "imports": {}}`), 0644) + assert.NoError(err) + + _, err = executeCmd(rootCmd, "dev", "--", "--port", "4321") + assert.NoError(err) + assert.True(mockCommandRunner.HasCommand("deno", "task", "dev", "--port", "4321")) + }) + + It("returns an error when deno dev args contain --eval", func() { + testDir := GinkgoT().TempDir() + originalDir, err := os.Getwd() + assert.NoError(err) + err = os.Chdir(testDir) + assert.NoError(err) + GinkgoT().Cleanup(func() { + if originalDir != "" { + _ = os.Chdir(originalDir) + } + }) + + rootCmd := factory.CreateDenoAsDefault(nil) + err = os.WriteFile(filepath.Join(testDir, "deno.json"), []byte(`{"tasks": {"dev": "deno task dev"}}`), 0644) + assert.NoError(err) + + _, err = executeCmd(rootCmd, "dev", "--", "--eval") + assert.Error(err) + assert.Contains(err.Error(), "don't pass --eval here use the exec command instead") + }) + + It("executes bun run dev", func() { + rootCmd := factory.CreateBunAsDefault(nil) + testDir := GinkgoT().TempDir() + originalDir, err := os.Getwd() + assert.NoError(err) + err = os.Chdir(testDir) + assert.NoError(err) + GinkgoT().Cleanup(func() { + if originalDir != "" { + _ = os.Chdir(originalDir) + } + }) + + err = os.WriteFile(filepath.Join(testDir, "package.json"), []byte(`{"scripts": {"dev": "bun dev"}}`), 0644) + assert.NoError(err) + err = os.WriteFile(filepath.Join(testDir, "bun.lockb"), []byte(""), 0644) + assert.NoError(err) + err = os.Mkdir(filepath.Join(testDir, "node_modules"), 0755) + assert.NoError(err) + + _, err = executeCmd(rootCmd, "dev", "--", "--hot") + assert.NoError(err) + assert.True(mockCommandRunner.HasCommand("bun", "run", "dev", "--hot")) }) }) @@ -4624,6 +4526,7 @@ var _ = Describe("JPD Commands", func() { assert.Contains(commandNames, "run") assert.Contains(commandNames, "exec") assert.Contains(commandNames, "dlx") + assert.Contains(commandNames, "dev") assert.Contains(commandNames, "create") assert.Contains(commandNames, "update") assert.Contains(commandNames, "uninstall") @@ -4641,7 +4544,7 @@ var _ = Describe("JPD Commands", func() { userCommands++ } } - assert.Equal(11, userCommands) + assert.Equal(12, userCommands) }) }) diff --git a/cmd/create.go b/cmd/create.go index 37a2288..31e8a7a 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -29,267 +29,32 @@ import ( "strings" // external - - "github.com/charmbracelet/huh" "github.com/charmbracelet/log" - "github.com/samber/lo" "github.com/spf13/cobra" - - // internal - "github.com/louiss0/javascript-package-delegator/custom_errors" - "github.com/louiss0/javascript-package-delegator/detect" - "github.com/louiss0/javascript-package-delegator/services" ) -// BuildCreateCommand builds command line for running package create commands -func BuildCreateCommand(pm, yarnVersion, name string, args []string) (program string, argv []string, err error) { - // Special handling for deno - requires URL as name - if pm == "deno" { - if name == "" { - return "", nil, fmt.Errorf("deno create requires a URL as the first argument") - } - if !isURL(name) { - return "", nil, fmt.Errorf("deno create requires a valid URL, got: %s", name) - } - return "deno", append([]string{"run", name}, args...), nil - } - - // For all other package managers, require name and reject URLs - if name == "" { - return "", nil, fmt.Errorf("package name is required for create command") - } - if isURL(name) { - return "", nil, fmt.Errorf("URLs are not supported for %s, use deno instead", pm) - } - - // Determine the actual binary to execute (create- for unscoped, leave scoped and already-prefixed names as-is) - bin := name - // identify scoped package like @scope/name (optionally with @version) - isScoped := strings.HasPrefix(name, "@") && strings.Contains(name, "/") - // strip version/dist tag to check base name prefixing rules - base := name - if at := strings.Index(base, "@"); at > 0 { // ignore leading '@' for scoped names - base = base[:at] - } - if !isScoped { - // only prefix for unscoped names when they don't already start with create- - if !strings.HasPrefix(base, "create-") { - bin = "create-" + name - } - } - - // Build command based on package manager - switch pm { - case "npm": - // npm exec create- -- - argv = append([]string{"exec", bin, "--"}, args...) - return "npm", argv, nil - case "pnpm": - // Prefer pnpm dlx for create runners: pnpm dlx - argv = append([]string{"dlx", bin}, args...) - return "pnpm", argv, nil - case "yarn": - yarnMajor := ParseYarnMajor(yarnVersion) - if yarnMajor <= 1 { - // Yarn v1 -> use npx: npx - argv = append([]string{bin}, args...) - return "npx", argv, nil - } else { - // Yarn v2+ -> use dlx: yarn dlx - argv = append([]string{"dlx", bin}, args...) - return "yarn", argv, nil - } - case "bun": - // bunx - argv = append([]string{bin}, args...) - return "bunx", argv, nil - default: - return "", nil, fmt.Errorf("unsupported package manager: %s", pm) - } -} - -// CreateAppSelector provides an interface for selecting a create app package. -// It follows Go Writing Philosophy: defined at point of use, with clean methods. -type CreateAppSelector interface { - Run() error - Value() string -} - -// CreateAppSearcher is a minimal interface to search for create-* packages. -// Defined at point of consumption for testability. -type CreateAppSearcher interface { - SearchCreateApps(query string, size int) ([]services.PackageInfo, error) -} - -// createAppSelector is a private struct implementing CreateAppSelector. -// All fields are unexported to comply with Go Writing Philosophy. -type createAppSelector struct { - sel *huh.Select[string] - value string -} - -// NewCreateAppSelector creates a new CreateAppSelector with pre-fetched packages and title. -// Constructor returns the interface (struct stays private) following Go Writing Philosophy. -func NewCreateAppSelector(packageInfo []services.PackageInfo) CreateAppSelector { - - // Map []services.PackageInfo to []huh.Option[string] using lo.Map - opts := lo.Map(packageInfo, func(p services.PackageInfo, _ int) huh.Option[string] { - // Use Name for both label and value; add description for better UX - return huh.NewOption(p.Name, fmt.Sprintf("%s %s", p.Name, p.Description)) - }) - - // Build the huh.Select with Title and Options - sel := huh.NewSelect[string](). - Title("Select app package"). - Options(opts...) - - // Return createAppSelector as CreateAppSelector interface - return &createAppSelector{sel: sel} -} - -// Run executes the interactive UI and stores the selected value. -// Uses pointer receiver since Run mutates internal state via s.value. -func (s *createAppSelector) Run() error { - // Bind the pointer - huh.Select.Value takes *string - s.sel.Value(&s.value) - return s.sel.Run() -} - -// Value returns the selected package name. -// No Get prefix - follows Go naming conventions. -func (s createAppSelector) Value() string { - return s.value -} - -// NewCreateCmd creates a new Cobra command for the "create" functionality -func NewCreateCmd(searcher CreateAppSearcher, newCreateAppSelector func([]services.PackageInfo) CreateAppSelector) *cobra.Command { - cmd := &cobra.Command{ - Use: "create [name|url] [args...]", - Short: "Scaffold a new project using create runners", - Long: `Scaffold a new project using the appropriate package manager's create command. -This command delegates to the package manager's create functionality to bootstrap new projects. +// NewCreateCmd creates a new Cobra command for the "create" subcommand. +// It delegates directly to the package manager's native create command. +func NewCreateCmd() *cobra.Command { + return &cobra.Command{ + Use: "create [args...]", + Short: "Scaffold a new project using the package manager's create command", + Long: `Scaffold a new project by delegating to the package manager's native create command. Package Manager Behavior: -- npm: Runs 'npm exec create- -- ' -- pnpm: Runs 'pnpm exec create- ' -- yarn v1: Runs 'npx create- ' -- yarn v2+: Runs 'yarn dlx create- ' -- bun: Runs 'bunx create- ' -- deno: Runs 'deno run ' (expects URL as first argument) - -JPD flags (for this command): - --search, -s Search npm for popular "create-*" packages and select interactively - --size Number of results to show when using --search (default: 25) - -Passing flags to scaffolding tools: -- npm: JPD automatically inserts the -- separator before the app name so flags go to the scaffolder. - Do not add another -- yourself; if you do, JPD will normalize it. -- pnpm / yarn v2+ / bun: pass flags directly after your app name. -- yarn v1: uses npx under the hood; pass flags directly after your app name. -- deno: pass arguments directly after the URL. +- npm: Runs 'npm create ' +- pnpm: Runs 'pnpm create ' +- yarn: Runs 'yarn create ' +- bun: Runs 'bun create ' +- deno: Runs 'deno init ' Examples: - jpd create react-app my-app - jpd create vite@latest my-app -- --template react-swc - jpd create next-app myapp --typescript --tailwind - jpd -a deno create https://deno.land/x/fresh/init.ts my-fresh-app`, + jpd create vite@latest my-app + jpd create next-app myapp --typescript + jpd -a deno create my-deno-app`, Aliases: []string{"c"}, - // Allow passing through unknown flags (e.g., flags intended for the underlying create tools) - FParseErrWhitelist: cobra.FParseErrWhitelist{ - UnknownFlags: true, - }, - DisableFlagParsing: true, - Args: cobra.ArbitraryArgs, + Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - // Manually parse flags since we disabled flag parsing - search := false - size := 0 - createAppQuery := "" - packageArgs := []string{} - - // Parse arguments manually to separate flags from create arguments - for i := 0; i < len(args); i++ { - arg := args[i] - switch { - case strings.HasPrefix(arg, "--search="): - search = true - value := strings.TrimPrefix(arg, "--search=") - if value == "" { - return fmt.Errorf("flag needs an argument: --search") - } - if createAppQuery != "" { - return fmt.Errorf("when using the --search flag, you cannot pass any other arguments") - } - createAppQuery = value - case strings.HasPrefix(arg, "-s="): - search = true - value := strings.TrimPrefix(arg, "-s=") - if value == "" { - return fmt.Errorf("flag needs an argument: --search") - } - if createAppQuery != "" { - return fmt.Errorf("when using the --search flag, you cannot pass any other arguments") - } - createAppQuery = value - case arg == "--search" || arg == "-s": - search = true - if createAppQuery != "" { - return fmt.Errorf("when using the --search flag, you cannot pass any other arguments") - } - if i+1 >= len(args) || strings.HasPrefix(args[i+1], "-") { - return fmt.Errorf("flag needs an argument: --search") - } - i++ - createAppQuery = args[i] - case arg == "--size": - if i+1 < len(args) { - i++ - if s, err := fmt.Sscanf(args[i], "%d", &size); err != nil || s != 1 { - return fmt.Errorf("invalid size value: %s", args[i]) - } - } else { - return fmt.Errorf("--size requires a value") - } - case arg == "-h" || arg == "--help": - return cmd.Help() - // Skip global flags - they're handled by the root command - case arg == "-a" || arg == "--agent": - if arg == "-a" || arg == "--agent" { - i++ // skip the value - } - case arg == "-C" || arg == "--cwd": - if arg == "-C" || arg == "--cwd" { - i++ // skip the value - } - case arg == "-d" || arg == "--debug": - // skip debug flag - case strings.HasPrefix(arg, "-a=") || strings.HasPrefix(arg, "--agent="): - // skip combined flag=value - case strings.HasPrefix(arg, "-C=") || strings.HasPrefix(arg, "--cwd="): - // skip combined flag=value - default: - // This is either the package name or arguments to pass through - if createAppQuery == "" { - createAppQuery = arg - } else { - packageArgs = append(packageArgs, arg) - } - } - } - - // Validate arguments - if search && createAppQuery == "" { - return fmt.Errorf("flag needs an argument: --search") - } - - if search && len(packageArgs) > 0 { - return fmt.Errorf("when using the --search flag, you cannot pass any other arguments") - } - - if !search && createAppQuery == "" { - return fmt.Errorf("requires at least 1 arg(s), only received 0") - } - pm, _ := cmd.Flags().GetString(AGENT_FLAG) goEnv := getGoEnvFromCommandContext(cmd) cmdRunner := getCommandRunnerFromCommandContext(cmd) @@ -299,102 +64,20 @@ Examples: log.Info("Using package manager", "pm", pm) }) - // Variables are already parsed above - - if search { - - goEnv.ExecuteIfModeIsProduction(func() { - log.Info( - "Searching for create app packages based on your search", - "search", - createAppQuery, - ) - }) - packageInfo, err := searcher.SearchCreateApps(createAppQuery, size) - - if err != nil { - return err - } - - if len(packageInfo) == 0 { - return custom_errors.CreateInvalidArgumentErrorWithMessage( - fmt.Sprintf("no packages found matching: %s", createAppQuery), - ) - } - - // Use new CreateAppSelector implementation - selector := newCreateAppSelector(packageInfo) - - if err := selector.Run(); err != nil { - return err - } - - chosen := selector.Value() - createAppQuery = strings.Split(chosen, " ")[0] - - } - - if createAppQuery == "" { - - goEnv.ExecuteIfModeIsProduction(func() { - log.Info( - "You have not provided a package to create from, searching for popular create app packages instead", - ) - }) - - packageInfo, err := searcher.SearchCreateApps(createAppQuery, size) - - if err != nil { - return err - } - - if len(packageInfo) == 0 { - return custom_errors.CreateInvalidArgumentErrorWithMessage( - fmt.Sprintf("no packages found matching: %s", createAppQuery), - ) - } - - // Use new CreateAppSelector implementation - selector := newCreateAppSelector(packageInfo) - - if err := selector.Run(); err != nil { - return err - } - - chosen := selector.Value() - createAppQuery = strings.Split(chosen, " ")[0] + var execCommand string + var cmdArgs []string + + switch pm { + case "npm", "pnpm", "yarn", "bun": + execCommand = pm + cmdArgs = append([]string{"create"}, args...) + case "deno": + execCommand = "deno" + cmdArgs = append([]string{"init"}, args...) + default: + return fmt.Errorf("unsupported package manager: %s", pm) } - // Get yarn version if needed - yarnVersion := "" - if pm == "yarn" { - if version, err := detect.DetectYarnVersion( - getYarnVersionRunnerCommandContext(cmd), - ); err == nil { - yarnVersion = version - } - } - - // Normalize extra "--" for npm (users sometimes add it themselves). - // npm mapping already injects one "--" internally, so remove any user-provided separators. - if pm == "npm" && len(packageArgs) > 0 { - filtered := make([]string, 0, len(packageArgs)) - for _, a := range packageArgs { - if a == "--" { - continue // drop - } - filtered = append(filtered, a) - } - packageArgs = filtered - } - - // Build command for creating projects - execCommand, cmdArgs, err := BuildCreateCommand(pm, yarnVersion, createAppQuery, packageArgs) - if err != nil { - return err - } - - // Execute the command de.LogJSCommandIfDebugIsTrue(execCommand, cmdArgs...) cmdRunner.Command(execCommand, cmdArgs...) @@ -405,10 +88,4 @@ Examples: return cmdRunner.Run() }, } - - // Add help-visible flags (parsing remains manual to allow passthrough) - cmd.Flags().BoolP("search", "s", false, "Search npm for create packages (interactive)") - cmd.Flags().Int("size", 25, "Number of results to show with --search") - - return cmd } diff --git a/cmd/dev.go b/cmd/dev.go new file mode 100644 index 0000000..ba7b9a9 --- /dev/null +++ b/cmd/dev.go @@ -0,0 +1,123 @@ +/* +Copyright © 2025 Shelton Louis + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +*/ + +// Package cmd provides command-line interface implementations for the JavaScript package delegator. +package cmd + +import ( + "fmt" + "os" + "strings" + + "github.com/charmbracelet/log" + "github.com/samber/lo" + "github.com/spf13/cobra" +) + +// NewDevCmd creates a new Cobra command for the "dev" subcommand. +// It starts the development server after ensuring dependencies are installed. +func NewDevCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "dev [args...]", + Short: "Run the dev script with automatic dependency checks", + Long: `Dev runs the dev script after ensuring dependencies are installed +for the detected package manager. It checks for missing node_modules and +dependency changes before starting the development server. + +Package Manager Behavior: +- npm: Runs 'npm run dev [-- args]' +- pnpm: Runs 'pnpm run dev [-- args]' +- yarn: Runs 'yarn run dev [args]' +- bun: Runs 'bun run dev [args]' +- deno: Runs 'deno task dev [args]' + +Examples: + jpd dev + jpd dev -- --host 0.0.0.0 + jpd dev -- --port 3001`, + Args: cobra.ArbitraryArgs, + RunE: func(cmd *cobra.Command, args []string) error { + pm, _ := cmd.Flags().GetString(AGENT_FLAG) + if pm == "" { + return fmt.Errorf("no package manager detected for dev command") + } + + noVoltaFlag, err := cmd.Flags().GetBool("no-volta") + if err != nil { + return fmt.Errorf("failed to parse --no-volta flag: %w", err) + } + + 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) + } + } + + cmdRunner := getCommandRunnerFromCommandContext(cmd) + goEnv := getGoEnvFromCommandContext(cmd) + de := getDebugExecutorFromCommandContext(cmd) + + if err := autoInstallDependenciesIfNeeded(pm, "dev", targetDir, cmdRunner, goEnv, de, noVoltaFlag); err != nil { + return err + } + + var cmdArgs []string + switch pm { + case "npm", "pnpm": + cmdArgs = []string{"run", "dev"} + if len(args) > 0 { + cmdArgs = append(cmdArgs, "--") + cmdArgs = append(cmdArgs, args...) + } + case "yarn", "bun": + cmdArgs = []string{"run", "dev"} + cmdArgs = append(cmdArgs, args...) + case "deno": + if lo.Contains(args, "--eval") { + return fmt.Errorf("don't pass --eval here use the exec command instead") + } + cmdArgs = []string{"task", "dev"} + cmdArgs = append(cmdArgs, args...) + default: + return fmt.Errorf("dev command does not support package manager %q", pm) + } + + cmdRunner.Command(pm, cmdArgs...) + de.LogJSCommandIfDebugIsTrue(pm, cmdArgs...) + + goEnv.ExecuteIfModeIsProduction(func() { + log.Info("Running command", "pm", pm, "args", strings.Join(cmdArgs, " ")) + }) + + return cmdRunner.Run() + }, + } + + cmd.Flags().Bool("no-volta", false, "Disable Volta integration during auto-install") + + return cmd +} diff --git a/cmd/root.go b/cmd/root.go index 4f5f99f..6206b49 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -166,8 +166,6 @@ type Dependencies struct { NewPackageMultiSelectUI func([]services.PackageInfo) MultiUISelecter NewTaskSelectorUI func(options []string) TaskUISelector NewDependencyMultiSelectUI func(options []string) DependencyUIMultiSelector - NewCreateAppSelector func([]services.PackageInfo) CreateAppSelector - NewCreateAppSearcher func() CreateAppSearcher NewDebugExecutor func(bool) DebugExecutor } @@ -296,7 +294,8 @@ Available commands: run - Run package.json scripts (equivalent to 'nr') exec - Execute packages (equivalent to 'nlx') dlx - Execute packages with package runner (dedicated package-runner command) - create - Scaffold new projects (equivalent to 'npm|pnpm|yarn|bun create', 'deno run ') + dev - Run the dev script with automatic dependency checks + create - Scaffold new projects by delegating to 'npm|pnpm|yarn|bun create' or 'deno init' update - Update packages (equivalent to 'nup') uninstall - Uninstall packages (equivalent to 'nun') clean-install - Clean install with frozen lockfile (equivalent to 'nci') @@ -548,16 +547,10 @@ Available commands: cmd.AddCommand(NewInstallCmd(deps.DetectVolta, deps.NewPackageMultiSelectUI)) cmd.AddCommand(NewRunCmd(deps.NewTaskSelectorUI)) cmd.AddCommand(NewStartCmd()) + cmd.AddCommand(NewDevCmd()) cmd.AddCommand(NewExecCmd()) cmd.AddCommand(NewDlxCmd()) - // Defensive: default to real service if test deps didn’t set NewCreateAppSearcher - var cas CreateAppSearcher - if deps.NewCreateAppSearcher != nil { - cas = deps.NewCreateAppSearcher() - } else { - cas = services.NewNpmRegistryService() - } - cmd.AddCommand(NewCreateCmd(cas, deps.NewCreateAppSelector)) + cmd.AddCommand(NewCreateCmd()) cmd.AddCommand(NewUpdateCmd()) cmd.AddCommand(NewUninstallCmd(deps.NewDependencyMultiSelectUI)) cmd.AddCommand(NewCleanInstallCmd(deps.DetectVolta)) @@ -606,11 +599,7 @@ func init() { NewPackageMultiSelectUI: newPackageMultiSelectUI, NewTaskSelectorUI: newTaskSelectorUI, NewDependencyMultiSelectUI: newDependencySelectorUI, - NewCreateAppSelector: NewCreateAppSelector, - NewCreateAppSearcher: func() CreateAppSearcher { - return services.NewNpmRegistryService() - }, - NewDebugExecutor: newDebugExecutor, + NewDebugExecutor: newDebugExecutor, }, ) } diff --git a/testutil/pkg.go b/testutil/pkg.go index 7ace904..924c1a7 100644 --- a/testutil/pkg.go +++ b/testutil/pkg.go @@ -3,7 +3,6 @@ package testutil import ( "fmt" "os" - "strings" . "github.com/onsi/ginkgo/v2" "github.com/spf13/cobra" @@ -349,21 +348,6 @@ func (f *RootCommandFactory) baseDependencies() cmd.Dependencies { NewPackageMultiSelectUI: mock.NewMockPackageMultiSelectUI, NewTaskSelectorUI: mock.NewMockTaskSelectUI, NewDependencyMultiSelectUI: mock.NewMockDependencySelectUI, - NewCreateAppSelector: func(packageInfo []services.PackageInfo) cmd.CreateAppSelector { - - mockSelector := &mock.CreateAppSelectorMock{} - // Default mock behavior: select the first package - selectedPackage := packageInfo[0].Name - mockSelector.On("Run").Return(nil).Maybe() - mockSelector.On("Value").Return(selectedPackage).Maybe() - return mockSelector - }, - NewCreateAppSearcher: func() cmd.CreateAppSearcher { - m := &mock.CreateAppSearcherMock{} - m.On("SearchCreateApps", tmock.Anything, tmock.Anything). - Return([]services.PackageInfo{{Name: "create-vite@latest", Description: "Vite"}}, nil).Maybe() - return m - }, } } @@ -562,17 +546,6 @@ func (f *RootCommandFactory) CreateWithPackageManagerAndMultiSelectUI() *cobra.C deps.NewPackageMultiSelectUI = func(pi []services.PackageInfo) cmd.MultiUISelecter { return mock.NewMockPackageMultiSelectUI(pi) } - // Provide a searcher that returns empty for queries containing "nonexistent" - deps.NewCreateAppSearcher = func() cmd.CreateAppSearcher { - m := &mock.CreateAppSearcherMock{} - // Register specific matcher first so it has priority over the generic one - m.On("SearchCreateApps", tmock.MatchedBy(func(q string) bool { return strings.Contains(q, "nonexistent") }), tmock.Anything). - Return([]services.PackageInfo{}, nil).Maybe() - m.On("SearchCreateApps", tmock.Anything, tmock.Anything).Return([]services.PackageInfo{ - {Name: "create-vite@latest", Description: "Vite"}, - }, nil).Maybe() - return m - } return cmd.NewRootCmdForTesting(deps) } From 4293875b94c35dc921d91142469a403310ffe071 Mon Sep 17 00:00:00 2001 From: shelton louis Date: Thu, 16 Apr 2026 03:20:46 +0000 Subject: [PATCH 2/7] fix: use 'deno create' for deno scaffolding; add yarn v1/v2+ tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Research confirms ALL package managers support a 'create' command: - npm: 'npm create ' (alias for npm init, runs create-* packages) - pnpm: 'pnpm create ' - yarn v1: 'yarn create ' (yarn invented create in 2017, npm copied it) - yarn v2+: 'yarn create ' - bun: 'bun create ' - deno: 'deno create ' (added Feb 2026, aligns with other PMs) Previously used 'deno init' which only scaffolds a blank project — 'deno create' is the correct equivalent for running create-* packages. Test additions: - create: add yarn v2+ test (was only testing v1) - create: update deno test to use 'deno create' - dev: add explicit yarn v1 test alongside existing yarn v2 test Note: dev command tests already existed in the DevCommand Describe block covering npm, pnpm, yarn v2, bun, and deno. Co-Authored-By: Oz --- cmd/cmd_test.go | 46 ++++++++++++++++++++++++++++++++++++++++------ cmd/create.go | 12 ++++++------ 2 files changed, 46 insertions(+), 12 deletions(-) diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go index 2cf19be..3b0f808 100644 --- a/cmd/cmd_test.go +++ b/cmd/cmd_test.go @@ -1646,7 +1646,7 @@ var _ = Describe("JPD Commands", func() { }) Context("yarn", func() { - It("delegates to yarn create for both v1 and v2+", func() { + It("delegates to yarn create for yarn v1", func() { yarnRootCmd := factory.CreateYarnOneAsDefault(nil) DebugExecutorExpectationManager.ExpectCommonPathDetectionFlow(detect.YARN) DebugExecutorExpectationManager.ExpectJSCommandLog("yarn", "create", "react-app", "my-app") @@ -1654,6 +1654,15 @@ var _ = Describe("JPD Commands", func() { assert.NoError(err) assert.True(mockCommandRunner.HasCommand("yarn", "create", "react-app", "my-app")) }) + + It("delegates to yarn create for yarn v2+", func() { + yarnRootCmd := factory.CreateYarnTwoAsDefault(nil) + DebugExecutorExpectationManager.ExpectCommonPMDetectionFlow(detect.YARN, detect.YARN_LOCK) + DebugExecutorExpectationManager.ExpectJSCommandLog("yarn", "create", "vite@latest", "my-app") + _, err := executeCmd(yarnRootCmd, "create", "vite@latest", "my-app") + assert.NoError(err) + assert.True(mockCommandRunner.HasCommand("yarn", "create", "vite@latest", "my-app")) + }) }) Context("bun", func() { @@ -1668,13 +1677,13 @@ var _ = Describe("JPD Commands", func() { }) Context("deno", func() { - It("delegates to deno init with args", func() { + It("delegates to deno create with args", func() { denoRootCmd := factory.CreateDenoAsDefault(nil) DebugExecutorExpectationManager.ExpectCommonPMDetectionFlow(detect.DENO, detect.DENO_JSON) - DebugExecutorExpectationManager.ExpectJSCommandLog("deno", "init", "my-deno-app") - _, err := executeCmd(denoRootCmd, "create", "my-deno-app") + DebugExecutorExpectationManager.ExpectJSCommandLog("deno", "create", "npm:vite", "my-deno-app") + _, err := executeCmd(denoRootCmd, "create", "npm:vite", "my-deno-app") assert.NoError(err) - assert.True(mockCommandRunner.HasCommand("deno", "init", "my-deno-app")) + assert.True(mockCommandRunner.HasCommand("deno", "create", "npm:vite", "my-deno-app")) }) }) @@ -1764,7 +1773,7 @@ var _ = Describe("JPD Commands", func() { assert.True(mockCommandRunner.HasCommand("pnpm", "run", "dev")) }) - It("passes through args without double dash for yarn", func() { + It("passes through args without double dash for yarn v2+", func() { rootCmd := factory.CreateYarnTwoAsDefault(nil) testDir := GinkgoT().TempDir() originalDir, err := os.Getwd() @@ -1789,6 +1798,31 @@ var _ = Describe("JPD Commands", func() { assert.True(mockCommandRunner.HasCommand("yarn", "run", "dev", "--inspect")) }) + It("passes through args without double dash for yarn v1", func() { + rootCmd := factory.CreateYarnOneAsDefault(nil) + testDir := GinkgoT().TempDir() + originalDir, err := os.Getwd() + assert.NoError(err) + err = os.Chdir(testDir) + assert.NoError(err) + GinkgoT().Cleanup(func() { + if originalDir != "" { + _ = os.Chdir(originalDir) + } + }) + + err = os.WriteFile(filepath.Join(testDir, "package.json"), []byte(`{"scripts": {"dev": "yarn dev"}}`), 0644) + assert.NoError(err) + err = os.Mkdir(filepath.Join(testDir, "node_modules"), 0755) + assert.NoError(err) + + DebugExecutorExpectationManager.ExpectNoLockfile() + DebugExecutorExpectationManager.ExpectPMDetectedFromPath(detect.YARN) + _, err = executeCmd(rootCmd, "dev", "--", "--port", "3000") + assert.NoError(err) + assert.True(mockCommandRunner.HasCommand("yarn", "run", "dev", "--port", "3000")) + }) + It("runs deno task dev", func() { testDir := GinkgoT().TempDir() originalDir, err := os.Getwd() diff --git a/cmd/create.go b/cmd/create.go index 31e8a7a..4c86399 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -39,19 +39,19 @@ func NewCreateCmd() *cobra.Command { return &cobra.Command{ Use: "create [args...]", Short: "Scaffold a new project using the package manager's create command", - Long: `Scaffold a new project by delegating to the package manager's native create command. +Long: `Scaffold a new project by delegating to the package manager's native create command. Package Manager Behavior: -- npm: Runs 'npm create ' +- npm: Runs 'npm create ' (npm create = npm init, runs create-* packages) - pnpm: Runs 'pnpm create ' -- yarn: Runs 'yarn create ' +- yarn: Runs 'yarn create ' (works for both yarn v1 and v2+) - bun: Runs 'bun create ' -- deno: Runs 'deno init ' +- deno: Runs 'deno create ' Examples: jpd create vite@latest my-app jpd create next-app myapp --typescript - jpd -a deno create my-deno-app`, + jpd -a deno create npm:vite my-deno-app`, Aliases: []string{"c"}, Args: cobra.MinimumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { @@ -73,7 +73,7 @@ Examples: cmdArgs = append([]string{"create"}, args...) case "deno": execCommand = "deno" - cmdArgs = append([]string{"init"}, args...) + cmdArgs = append([]string{"create"}, args...) default: return fmt.Errorf("unsupported package manager: %s", pm) } From 8782b29f09a17c0771349918fb893643ab2aee52 Mon Sep 17 00:00:00 2001 From: shelton louis Date: Thu, 16 Apr 2026 03:24:18 +0000 Subject: [PATCH 3/7] style: gofmt and goimports Co-Authored-By: Oz --- cmd/create.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/create.go b/cmd/create.go index 4c86399..533be99 100644 --- a/cmd/create.go +++ b/cmd/create.go @@ -39,7 +39,7 @@ func NewCreateCmd() *cobra.Command { return &cobra.Command{ Use: "create [args...]", Short: "Scaffold a new project using the package manager's create command", -Long: `Scaffold a new project by delegating to the package manager's native create command. + Long: `Scaffold a new project by delegating to the package manager's native create command. Package Manager Behavior: - npm: Runs 'npm create ' (npm create = npm init, runs create-* packages) From 83fb79a847f09f43589eaa76b8647aba99f7bd6c Mon Sep 17 00:00:00 2001 From: shelton louis Date: Thu, 16 Apr 2026 03:27:44 +0000 Subject: [PATCH 4/7] fix(lint): resolve golangci-lint staticcheck errors - internal/alias.go: replace WriteString(fmt.Sprintf(...)) with fmt.Fprintf(&result, ...) on lines 52, 54, 76 (QF1012) - testutil/pkg.go: replace dot import of ginkgo/v2 with named import; qualify DeferCleanup as ginkgo.DeferCleanup (ST1001) Co-Authored-By: Oz --- internal/alias.go | 6 +++--- testutil/pkg.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/alias.go b/internal/alias.go index 240c4f7..202225f 100644 --- a/internal/alias.go +++ b/internal/alias.go @@ -49,9 +49,9 @@ func (g *aliasGenerator) GenerateBash(aliases map[string][]string) string { for subcommand, aliasNames := range aliases { for _, aliasName := range aliasNames { // Generate function - result.WriteString(fmt.Sprintf("function %s() { command jpd %s \"$@\"; }\n", aliasName, subcommand)) + fmt.Fprintf(&result, "function %s() { command jpd %s \"$@\"; }\n", aliasName, subcommand) // Generate completion wiring - result.WriteString(fmt.Sprintf("complete -F __start_jpd %s\n", aliasName)) + fmt.Fprintf(&result, "complete -F __start_jpd %s\n", aliasName) result.WriteString("\n") } } @@ -73,7 +73,7 @@ func (g *aliasGenerator) GenerateZsh(aliases map[string][]string) string { for subcommand, aliasNames := range aliases { for _, aliasName := range aliasNames { // Generate function - result.WriteString(fmt.Sprintf("%s() { jpd %s \"$@\"; }\n", aliasName, subcommand)) + fmt.Fprintf(&result, "%s() { jpd %s \"$@\"; }\n", aliasName, subcommand) // Generate completion wiring result.WriteString(fmt.Sprintf("compdef _jpd %s\n", aliasName)) result.WriteString("\n") diff --git a/testutil/pkg.go b/testutil/pkg.go index 924c1a7..c74b1ea 100644 --- a/testutil/pkg.go +++ b/testutil/pkg.go @@ -4,7 +4,7 @@ import ( "fmt" "os" - . "github.com/onsi/ginkgo/v2" + ginkgo "github.com/onsi/ginkgo/v2" "github.com/spf13/cobra" tmock "github.com/stretchr/testify/mock" @@ -435,7 +435,7 @@ func (f *RootCommandFactory) CreateDenoAsDefault(err error) *cobra.Command { } }` _ = os.WriteFile("deno.json", []byte(denoJSONContent), 0644) - DeferCleanup(func() { + ginkgo.DeferCleanup(func() { _ = os.Remove("deno.json") }) From a6235edfce037f1c420c975aafc6897037d9cab9 Mon Sep 17 00:00:00 2001 From: shelton louis Date: Thu, 16 Apr 2026 03:32:13 +0000 Subject: [PATCH 5/7] fix(lint): replace remaining WriteString(fmt.Sprintf(...)) in alias.go (QF1012) Converted all remaining occurrences across GenerateZsh, GenerateFish, GenerateNushell, and GeneratePowerShell to fmt.Fprintf(&result, ...). Co-Authored-By: Oz --- internal/alias.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/internal/alias.go b/internal/alias.go index 202225f..4d20090 100644 --- a/internal/alias.go +++ b/internal/alias.go @@ -75,7 +75,7 @@ func (g *aliasGenerator) GenerateZsh(aliases map[string][]string) string { // Generate function fmt.Fprintf(&result, "%s() { jpd %s \"$@\"; }\n", aliasName, subcommand) // Generate completion wiring - result.WriteString(fmt.Sprintf("compdef _jpd %s\n", aliasName)) + fmt.Fprintf(&result, "compdef _jpd %s\n", aliasName) result.WriteString("\n") } } @@ -94,11 +94,11 @@ func (g *aliasGenerator) GenerateFish(aliases map[string][]string) string { for subcommand, aliasNames := range aliases { for _, aliasName := range aliasNames { // Generate function - result.WriteString(fmt.Sprintf("function %s\n", aliasName)) - result.WriteString(fmt.Sprintf(" jpd %s $argv\n", subcommand)) + fmt.Fprintf(&result, "function %s\n", aliasName) + fmt.Fprintf(&result, " jpd %s $argv\n", subcommand) result.WriteString("end\n") // Generate completion wiring - result.WriteString(fmt.Sprintf("complete -c %s -w jpd\n", aliasName)) + fmt.Fprintf(&result, "complete -c %s -w jpd\n", aliasName) result.WriteString("\n") } } @@ -117,12 +117,12 @@ func (g *aliasGenerator) GenerateNushell(aliases map[string][]string) string { for subcommand, aliasNames := range aliases { for _, aliasName := range aliasNames { // Generate extern declaration - result.WriteString(fmt.Sprintf("export extern \"%s\" [\n", aliasName)) + fmt.Fprintf(&result, "export extern \"%s\" [\n", aliasName) result.WriteString(" ...args: string\n") result.WriteString("]\n") // Generate function definition - result.WriteString(fmt.Sprintf("export def %s [...args] {\n", aliasName)) - result.WriteString(fmt.Sprintf(" jpd %s $args\n", subcommand)) + fmt.Fprintf(&result, "export def %s [...args] {\n", aliasName) + fmt.Fprintf(&result, " jpd %s $args\n", subcommand) result.WriteString("}\n\n") } } @@ -146,11 +146,11 @@ func (g *aliasGenerator) GeneratePowerShell(aliases map[string][]string) string for subcommand, aliasNames := range aliases { for _, aliasName := range aliasNames { // Generate function - result.WriteString(fmt.Sprintf("function %s {\n", aliasName)) - result.WriteString(fmt.Sprintf(" jpd %s @args\n", subcommand)) + fmt.Fprintf(&result, "function %s {\n", aliasName) + fmt.Fprintf(&result, " jpd %s @args\n", subcommand) result.WriteString("}\n") // Generate completion registration - result.WriteString(fmt.Sprintf("Register-ArgumentCompleter -CommandName '%s' -ScriptBlock {\n", aliasName)) + fmt.Fprintf(&result, "Register-ArgumentCompleter -CommandName '%s' -ScriptBlock {\n", aliasName) result.WriteString(" param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)\n") result.WriteString(" $completions = @()\n") result.WriteString(" # Add basic jpd argument completion here\n") From 95a7e1ce446447f29126af7b69c1f458961efcc2 Mon Sep 17 00:00:00 2001 From: shelton louis Date: Thu, 16 Apr 2026 03:35:22 +0000 Subject: [PATCH 6/7] fix(lint): fmt.Fprintf in cmd/start.go (QF1012) Replace WriteString(fmt.Sprintf(...)) with fmt.Fprintf(&installReason, ...) on lines 223 and 289 of autoInstallDependenciesIfNeeded. Co-Authored-By: Oz --- cmd/start.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/start.go b/cmd/start.go index dae7e81..ca67ccd 100644 --- a/cmd/start.go +++ b/cmd/start.go @@ -220,7 +220,7 @@ func autoInstallDependenciesIfNeeded( missing := MissingNodePackages(baseDir, names) if len(missing) > 0 { shouldInstall = true - installReason.WriteString(fmt.Sprintf("%d missing packages; ", len(missing))) + fmt.Fprintf(&installReason, "%d missing packages; ", len(missing)) if goEnv.IsDevelopmentMode() { firstFew := missing @@ -286,7 +286,7 @@ func autoInstallDependenciesIfNeeded( if missingImports > 0 { shouldInstall = true - installReason.WriteString(fmt.Sprintf("%d unresolvable imports; ", missingImports)) + fmt.Fprintf(&installReason, "%d unresolvable imports; ", missingImports) if goEnv.IsDevelopmentMode() { de.LogDebugMessageIfDebugIsTrue("Import check", "checked", len(checksToRun), "missing", missingImports) From c250d08d094beb4c339b4a2ec2f84facec8ac452 Mon Sep 17 00:00:00 2001 From: shelton louis Date: Thu, 16 Apr 2026 03:44:37 +0000 Subject: [PATCH 7/7] feat: remove dev command and its tests Delete cmd/dev.go, unregister NewDevCmd from root.go, remove the DevCommand Describe block from cmd_test.go, and restore the command count to 11. Co-Authored-By: Oz --- cmd/cmd_test.go | 186 +----------------------------------------------- cmd/dev.go | 123 -------------------------------- cmd/root.go | 4 +- 3 files changed, 2 insertions(+), 311 deletions(-) delete mode 100644 cmd/dev.go diff --git a/cmd/cmd_test.go b/cmd/cmd_test.go index 3b0f808..59aa8ae 100644 --- a/cmd/cmd_test.go +++ b/cmd/cmd_test.go @@ -1709,189 +1709,6 @@ var _ = Describe("JPD Commands", func() { }) - const DevCommand = "Dev Command" - Describe(DevCommand, func() { - - It("should show help", func() { - output, err := executeCmd(rootCmd, "dev", "--help") - assert.NoError(err) - assert.Contains(output, "Dev runs the dev script after ensuring dependencies are installed") - assert.Contains(output, "jpd dev") - }) - - It("runs npm run dev and forwards extra arguments", func() { - rootCmd := factory.CreateNpmAsDefault(nil) - testDir := GinkgoT().TempDir() - originalDir, err := os.Getwd() - assert.NoError(err) - err = os.Chdir(testDir) - assert.NoError(err) - GinkgoT().Cleanup(func() { - if originalDir != "" { - _ = os.Chdir(originalDir) - } - }) - - err = os.WriteFile(filepath.Join(testDir, "package.json"), []byte(`{"scripts": {"dev": "echo dev"}}`), 0644) - assert.NoError(err) - err = os.WriteFile(filepath.Join(testDir, "package-lock.json"), []byte(""), 0644) - assert.NoError(err) - err = os.Mkdir(filepath.Join(testDir, "node_modules"), 0755) - assert.NoError(err) - - DebugExecutorExpectationManager.ExpectLockfileDetected(detect.PACKAGE_LOCK_JSON) - DebugExecutorExpectationManager.ExpectPMDetectedFromLockfile(detect.NPM) - _, err = executeCmd(rootCmd, "dev", "--", "--host", "0.0.0.0") - assert.NoError(err) - assert.True(mockCommandRunner.HasCommand("npm", "run", "dev", "--", "--host", "0.0.0.0")) - }) - - It("runs pnpm run dev with auto-install when node_modules missing", func() { - rootCmd := factory.CreatePnpmAsDefault(nil) - testDir := GinkgoT().TempDir() - originalDir, err := os.Getwd() - assert.NoError(err) - err = os.Chdir(testDir) - assert.NoError(err) - GinkgoT().Cleanup(func() { - if originalDir != "" { - _ = os.Chdir(originalDir) - } - }) - - err = os.WriteFile(filepath.Join(testDir, "package.json"), []byte(`{"scripts": {"dev": "echo dev"}}`), 0644) - assert.NoError(err) - err = os.WriteFile(filepath.Join(testDir, "pnpm-lock.yaml"), []byte(""), 0644) - assert.NoError(err) - - DebugExecutorExpectationManager.ExpectLockfileDetected(detect.PNPM_LOCK_YAML) - DebugExecutorExpectationManager.ExpectPMDetectedFromLockfile(detect.PNPM) - _, err = executeCmd(rootCmd, "dev") - assert.NoError(err) - - assert.True(mockCommandRunner.WasCommandCalled("pnpm", "install")) - assert.True(mockCommandRunner.HasCommand("pnpm", "run", "dev")) - }) - - It("passes through args without double dash for yarn v2+", func() { - rootCmd := factory.CreateYarnTwoAsDefault(nil) - testDir := GinkgoT().TempDir() - originalDir, err := os.Getwd() - assert.NoError(err) - err = os.Chdir(testDir) - assert.NoError(err) - GinkgoT().Cleanup(func() { - if originalDir != "" { - _ = os.Chdir(originalDir) - } - }) - - err = os.WriteFile(filepath.Join(testDir, "package.json"), []byte(`{"scripts": {"dev": "yarn dev"}}`), 0644) - assert.NoError(err) - err = os.WriteFile(filepath.Join(testDir, "yarn.lock"), []byte(""), 0644) - assert.NoError(err) - err = os.Mkdir(filepath.Join(testDir, "node_modules"), 0755) - assert.NoError(err) - - _, err = executeCmd(rootCmd, "dev", "--", "--inspect") - assert.NoError(err) - assert.True(mockCommandRunner.HasCommand("yarn", "run", "dev", "--inspect")) - }) - - It("passes through args without double dash for yarn v1", func() { - rootCmd := factory.CreateYarnOneAsDefault(nil) - testDir := GinkgoT().TempDir() - originalDir, err := os.Getwd() - assert.NoError(err) - err = os.Chdir(testDir) - assert.NoError(err) - GinkgoT().Cleanup(func() { - if originalDir != "" { - _ = os.Chdir(originalDir) - } - }) - - err = os.WriteFile(filepath.Join(testDir, "package.json"), []byte(`{"scripts": {"dev": "yarn dev"}}`), 0644) - assert.NoError(err) - err = os.Mkdir(filepath.Join(testDir, "node_modules"), 0755) - assert.NoError(err) - - DebugExecutorExpectationManager.ExpectNoLockfile() - DebugExecutorExpectationManager.ExpectPMDetectedFromPath(detect.YARN) - _, err = executeCmd(rootCmd, "dev", "--", "--port", "3000") - assert.NoError(err) - assert.True(mockCommandRunner.HasCommand("yarn", "run", "dev", "--port", "3000")) - }) - - It("runs deno task dev", func() { - testDir := GinkgoT().TempDir() - originalDir, err := os.Getwd() - assert.NoError(err) - err = os.Chdir(testDir) - assert.NoError(err) - GinkgoT().Cleanup(func() { - if originalDir != "" { - _ = os.Chdir(originalDir) - } - }) - - rootCmd := factory.CreateDenoAsDefault(nil) - err = os.WriteFile(filepath.Join(testDir, "deno.json"), []byte(`{"tasks": {"dev": "deno task dev"}, "imports": {}}`), 0644) - assert.NoError(err) - - _, err = executeCmd(rootCmd, "dev", "--", "--port", "4321") - assert.NoError(err) - assert.True(mockCommandRunner.HasCommand("deno", "task", "dev", "--port", "4321")) - }) - - It("returns an error when deno dev args contain --eval", func() { - testDir := GinkgoT().TempDir() - originalDir, err := os.Getwd() - assert.NoError(err) - err = os.Chdir(testDir) - assert.NoError(err) - GinkgoT().Cleanup(func() { - if originalDir != "" { - _ = os.Chdir(originalDir) - } - }) - - rootCmd := factory.CreateDenoAsDefault(nil) - err = os.WriteFile(filepath.Join(testDir, "deno.json"), []byte(`{"tasks": {"dev": "deno task dev"}}`), 0644) - assert.NoError(err) - - _, err = executeCmd(rootCmd, "dev", "--", "--eval") - assert.Error(err) - assert.Contains(err.Error(), "don't pass --eval here use the exec command instead") - }) - - It("executes bun run dev", func() { - rootCmd := factory.CreateBunAsDefault(nil) - testDir := GinkgoT().TempDir() - originalDir, err := os.Getwd() - assert.NoError(err) - err = os.Chdir(testDir) - assert.NoError(err) - GinkgoT().Cleanup(func() { - if originalDir != "" { - _ = os.Chdir(originalDir) - } - }) - - err = os.WriteFile(filepath.Join(testDir, "package.json"), []byte(`{"scripts": {"dev": "bun dev"}}`), 0644) - assert.NoError(err) - err = os.WriteFile(filepath.Join(testDir, "bun.lockb"), []byte(""), 0644) - assert.NoError(err) - err = os.Mkdir(filepath.Join(testDir, "node_modules"), 0755) - assert.NoError(err) - - _, err = executeCmd(rootCmd, "dev", "--", "--hot") - assert.NoError(err) - assert.True(mockCommandRunner.HasCommand("bun", "run", "dev", "--hot")) - }) - - }) - const AgentCommand = "Agent Command" Describe(AgentCommand, func() { @@ -4560,7 +4377,6 @@ var _ = Describe("JPD Commands", func() { assert.Contains(commandNames, "run") assert.Contains(commandNames, "exec") assert.Contains(commandNames, "dlx") - assert.Contains(commandNames, "dev") assert.Contains(commandNames, "create") assert.Contains(commandNames, "update") assert.Contains(commandNames, "uninstall") @@ -4578,7 +4394,7 @@ var _ = Describe("JPD Commands", func() { userCommands++ } } - assert.Equal(12, userCommands) + assert.Equal(11, userCommands) }) }) diff --git a/cmd/dev.go b/cmd/dev.go deleted file mode 100644 index ba7b9a9..0000000 --- a/cmd/dev.go +++ /dev/null @@ -1,123 +0,0 @@ -/* -Copyright © 2025 Shelton Louis - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -*/ - -// Package cmd provides command-line interface implementations for the JavaScript package delegator. -package cmd - -import ( - "fmt" - "os" - "strings" - - "github.com/charmbracelet/log" - "github.com/samber/lo" - "github.com/spf13/cobra" -) - -// NewDevCmd creates a new Cobra command for the "dev" subcommand. -// It starts the development server after ensuring dependencies are installed. -func NewDevCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "dev [args...]", - Short: "Run the dev script with automatic dependency checks", - Long: `Dev runs the dev script after ensuring dependencies are installed -for the detected package manager. It checks for missing node_modules and -dependency changes before starting the development server. - -Package Manager Behavior: -- npm: Runs 'npm run dev [-- args]' -- pnpm: Runs 'pnpm run dev [-- args]' -- yarn: Runs 'yarn run dev [args]' -- bun: Runs 'bun run dev [args]' -- deno: Runs 'deno task dev [args]' - -Examples: - jpd dev - jpd dev -- --host 0.0.0.0 - jpd dev -- --port 3001`, - Args: cobra.ArbitraryArgs, - RunE: func(cmd *cobra.Command, args []string) error { - pm, _ := cmd.Flags().GetString(AGENT_FLAG) - if pm == "" { - return fmt.Errorf("no package manager detected for dev command") - } - - noVoltaFlag, err := cmd.Flags().GetBool("no-volta") - if err != nil { - return fmt.Errorf("failed to parse --no-volta flag: %w", err) - } - - 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) - } - } - - cmdRunner := getCommandRunnerFromCommandContext(cmd) - goEnv := getGoEnvFromCommandContext(cmd) - de := getDebugExecutorFromCommandContext(cmd) - - if err := autoInstallDependenciesIfNeeded(pm, "dev", targetDir, cmdRunner, goEnv, de, noVoltaFlag); err != nil { - return err - } - - var cmdArgs []string - switch pm { - case "npm", "pnpm": - cmdArgs = []string{"run", "dev"} - if len(args) > 0 { - cmdArgs = append(cmdArgs, "--") - cmdArgs = append(cmdArgs, args...) - } - case "yarn", "bun": - cmdArgs = []string{"run", "dev"} - cmdArgs = append(cmdArgs, args...) - case "deno": - if lo.Contains(args, "--eval") { - return fmt.Errorf("don't pass --eval here use the exec command instead") - } - cmdArgs = []string{"task", "dev"} - cmdArgs = append(cmdArgs, args...) - default: - return fmt.Errorf("dev command does not support package manager %q", pm) - } - - cmdRunner.Command(pm, cmdArgs...) - de.LogJSCommandIfDebugIsTrue(pm, cmdArgs...) - - goEnv.ExecuteIfModeIsProduction(func() { - log.Info("Running command", "pm", pm, "args", strings.Join(cmdArgs, " ")) - }) - - return cmdRunner.Run() - }, - } - - cmd.Flags().Bool("no-volta", false, "Disable Volta integration during auto-install") - - return cmd -} diff --git a/cmd/root.go b/cmd/root.go index 6206b49..a15aec2 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -294,8 +294,7 @@ Available commands: run - Run package.json scripts (equivalent to 'nr') exec - Execute packages (equivalent to 'nlx') dlx - Execute packages with package runner (dedicated package-runner command) - dev - Run the dev script with automatic dependency checks - create - Scaffold new projects by delegating to 'npm|pnpm|yarn|bun create' or 'deno init' + create - Scaffold new projects by delegating to 'npm|pnpm|yarn|bun create' or 'deno create' update - Update packages (equivalent to 'nup') uninstall - Uninstall packages (equivalent to 'nun') clean-install - Clean install with frozen lockfile (equivalent to 'nci') @@ -547,7 +546,6 @@ Available commands: cmd.AddCommand(NewInstallCmd(deps.DetectVolta, deps.NewPackageMultiSelectUI)) cmd.AddCommand(NewRunCmd(deps.NewTaskSelectorUI)) cmd.AddCommand(NewStartCmd()) - cmd.AddCommand(NewDevCmd()) cmd.AddCommand(NewExecCmd()) cmd.AddCommand(NewDlxCmd()) cmd.AddCommand(NewCreateCmd())