diff --git a/internal/adapters/primary/cli/new.go b/internal/adapters/primary/cli/new.go
new file mode 100644
index 00000000..c9efe6b7
--- /dev/null
+++ b/internal/adapters/primary/cli/new.go
@@ -0,0 +1,236 @@
+// Package cli provides command-line interface implementation for Boss.
+package cli
+
+import (
+ "crypto/rand"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/hashload/boss/internal/core/domain"
+ "github.com/hashload/boss/pkg/consts"
+ "github.com/hashload/boss/pkg/msg"
+ "github.com/hashload/boss/pkg/pkgmanager"
+ "github.com/spf13/cobra"
+)
+
+var (
+ projectType string
+ quietNew bool
+)
+
+const dprTemplate = `program %s;
+
+{$APPTYPE CONSOLE}
+
+{$R *.res}
+
+uses
+ System.SysUtils;
+
+begin
+ try
+ Writeln('Hello from %s!');
+ except
+ on E: Exception do
+ Writeln(E.ClassName, ': ', E.Message);
+ end;
+end.
+`
+
+const dpkTemplate = `package %s;
+
+{$R *.res}
+{$IFDEF IMPLICITBUILDING}
+{$IMPLICITBUILD ON}
+{$ENDIF}
+
+requires
+ rtl;
+
+contains
+ // Add package units here
+ ;
+
+end.
+`
+
+const dprojTemplate = `
+
+ %s
+ 19.5
+ None
+ True
+ Debug
+ Win32
+ 1
+ %s
+
+
+ true
+
+
+ true
+ Base
+ true
+
+
+ true
+ Base
+ true
+
+
+ true
+ Base
+ true
+
+
+
+ MainSource
+
+
+ Cfg_2
+ Base
+
+
+ Base
+
+
+ Cfg_1
+ Base
+
+
+
+ Delphi.Personality.12
+ %s
+
+
+
+ %s.%s
+
+
+
+ 12
+
+
+
+
+`
+
+// generateGUID generates a random GUID in the standard {XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX} format.
+func generateGUID() string {
+ b := make([]byte, 16)
+ _, err := rand.Read(b)
+ if err != nil {
+ return "{00000000-0000-0000-0000-000000000000}"
+ }
+ return fmt.Sprintf("{%02X%02X%02X%02X-%02X%02X-%02X%02X-%02X%02X-%02X%02X%02X%02X%02X%02X}",
+ b[0], b[1], b[2], b[3], b[4], b[5], b[6], b[7], b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15])
+}
+
+// newCmdRegister registers the new command.
+func newCmdRegister(root *cobra.Command) {
+ var newCmd = &cobra.Command{
+ Use: "new [project_name]",
+ Short: "Create a new Delphi project skeleton",
+ Long: "Create a new Delphi project skeleton with source directories, templates, and boss.json",
+ Args: cobra.MaximumNArgs(1),
+ Example: ` Create a new console application:
+ boss new my_project
+
+ Create a new package/library:
+ boss new my_package --type pkg`,
+ Run: func(cmd *cobra.Command, args []string) {
+ var name string
+ if len(args) > 0 {
+ name = args[0]
+ }
+ doCreateProject(name, projectType, quietNew)
+ },
+ }
+
+ newCmd.Flags().StringVarP(&projectType, "type", "t", "app", "type of project to generate (app or pkg)")
+ newCmd.Flags().BoolVarP(&quietNew, "quiet", "q", false, "without asking questions")
+
+ root.AddCommand(newCmd)
+}
+
+// doCreateProject performs the project creation.
+func doCreateProject(name string, pType string, quiet bool) {
+ if !quiet && name == "" {
+ name = getParamOrDef("Project name", "")
+ }
+ name = strings.TrimSpace(name)
+ if name == "" {
+ msg.Die("❌ Project name is required.")
+ }
+
+ pType = strings.ToLower(strings.TrimSpace(pType))
+ if pType != "app" && pType != "pkg" {
+ msg.Die("❌ Invalid project type. Supported types: 'app' (default) or 'pkg'.")
+ }
+
+ cwd, err := os.Getwd()
+ if err != nil {
+ msg.Die("❌ Failed to get current working directory: %v", err)
+ }
+
+ projectDir := filepath.Join(cwd, name)
+ if _, err := os.Stat(projectDir); !os.IsNotExist(err) {
+ msg.Die("❌ Directory '%s' already exists.", name)
+ }
+
+ if !quiet {
+ msg.Info("🚀 Creating a new Delphi project skeleton in %s...", projectDir)
+ }
+
+ // Create directories
+ srcDir := filepath.Join(projectDir, "src")
+ testsDir := filepath.Join(projectDir, "tests")
+ if err := os.MkdirAll(srcDir, 0755); err != nil {
+ msg.Die("❌ Failed to create src directory: %v", err)
+ }
+ if err := os.MkdirAll(testsDir, 0755); err != nil {
+ msg.Die("❌ Failed to create tests directory: %v", err)
+ }
+
+ // Save boss.json
+ packageData := domain.NewPackage()
+ packageData.Name = name
+ packageData.Version = "1.0.0"
+ packageData.MainSrc = "src"
+
+ packageJsonPath := filepath.Join(projectDir, consts.FilePackage)
+ if err := pkgmanager.SavePackage(packageData, packageJsonPath); err != nil {
+ msg.Die("❌ Failed to save boss.json: %v", err)
+ }
+
+ // Write Delphi files
+ guid := generateGUID()
+ var dprojContent string
+ if pType == "app" {
+ dprPath := filepath.Join(projectDir, name+".dpr")
+ dprContent := fmt.Sprintf(dprTemplate, name, name)
+ if err := os.WriteFile(dprPath, []byte(dprContent), 0644); err != nil {
+ msg.Die("❌ Failed to create .dpr project file: %v", err)
+ }
+ dprojContent = fmt.Sprintf(dprojTemplate, guid, "Console", "Application", name, "dpr")
+ } else {
+ dpkPath := filepath.Join(projectDir, name+".dpk")
+ dpkContent := fmt.Sprintf(dpkTemplate, name)
+ if err := os.WriteFile(dpkPath, []byte(dpkContent), 0644); err != nil {
+ msg.Die("❌ Failed to create .dpk package file: %v", err)
+ }
+ dprojContent = fmt.Sprintf(dprojTemplate, guid, "Package", "Package", name, "dpk")
+ }
+
+ dprojPath := filepath.Join(projectDir, name+".dproj")
+ if err := os.WriteFile(dprojPath, []byte(dprojContent), 0644); err != nil {
+ msg.Die("❌ Failed to create .dproj configuration file: %v", err)
+ }
+
+ if !quiet {
+ msg.Info("✨ Project '%s' created successfully!", name)
+ }
+}
diff --git a/internal/adapters/primary/cli/new_test.go b/internal/adapters/primary/cli/new_test.go
new file mode 100644
index 00000000..8e17ccc6
--- /dev/null
+++ b/internal/adapters/primary/cli/new_test.go
@@ -0,0 +1,208 @@
+//nolint:testpackage // Testing internal command implementation
+package cli
+
+import (
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/hashload/boss/internal/adapters/secondary/filesystem"
+ "github.com/hashload/boss/internal/adapters/secondary/repository"
+ "github.com/hashload/boss/internal/core/domain"
+ "github.com/hashload/boss/internal/core/services/packages"
+ "github.com/hashload/boss/pkg/consts"
+ "github.com/hashload/boss/pkg/pkgmanager"
+ "github.com/spf13/cobra"
+)
+
+// TestNewCommandRegistration tests that the new command registers correctly.
+func TestNewCommandRegistration(t *testing.T) {
+ root := &cobra.Command{Use: "boss"}
+ newCmdRegister(root)
+
+ var newCmd *cobra.Command
+ for _, cmd := range root.Commands() {
+ if cmd.Use == "new [project_name]" {
+ newCmd = cmd
+ break
+ }
+ }
+
+ if newCmd == nil {
+ t.Fatal("New command not found")
+ }
+
+ if newCmd.Short == "" {
+ t.Error("New command should have a short description")
+ }
+
+ typeFlag := newCmd.Flags().Lookup("type")
+ if typeFlag == nil {
+ t.Error("New command should have --type flag")
+ }
+ if typeFlag.DefValue != "app" {
+ t.Errorf("New command --type flag default value should be 'app', got %s", typeFlag.DefValue)
+ }
+
+ quietFlag := newCmd.Flags().Lookup("quiet")
+ if quietFlag == nil {
+ t.Error("New command should have --quiet flag")
+ }
+}
+
+// TestDoCreateProject_App tests bootstrapping an application project.
+func TestDoCreateProject_App(t *testing.T) {
+ tempDir := t.TempDir()
+
+ // Redirect working directory
+ oldWd, err := os.Getwd()
+ if err == nil {
+ defer func() { _ = os.Chdir(oldWd) }()
+ }
+ if err := os.Chdir(tempDir); err != nil {
+ t.Fatalf("Failed to change directory: %v", err)
+ }
+
+ // Initialize package manager
+ fs := filesystem.NewOSFileSystem()
+ packageRepo := repository.NewFilePackageRepository(fs)
+ lockRepo := repository.NewFileLockRepository(fs)
+ packageService := packages.NewPackageService(packageRepo, lockRepo)
+ pkgmanager.SetInstance(packageService)
+
+ projectName := "testapp"
+ doCreateProject(projectName, "app", true)
+
+ projectPath := filepath.Join(tempDir, projectName)
+ if _, err := os.Stat(projectPath); os.IsNotExist(err) {
+ t.Fatalf("Project directory was not created")
+ }
+
+ // Check folders
+ if _, err := os.Stat(filepath.Join(projectPath, "src")); os.IsNotExist(err) {
+ t.Error("src directory was not created")
+ }
+ if _, err := os.Stat(filepath.Join(projectPath, "tests")); os.IsNotExist(err) {
+ t.Error("tests directory was not created")
+ }
+
+ // Check boss.json
+ bossJsonPath := filepath.Join(projectPath, consts.FilePackage)
+ if _, err := os.Stat(bossJsonPath); os.IsNotExist(err) {
+ t.Fatal("boss.json was not created")
+ }
+
+ bossBytes, err := os.ReadFile(bossJsonPath)
+ if err != nil {
+ t.Fatalf("Failed to read boss.json: %v", err)
+ }
+
+ var pkg domain.Package
+ if err := json.Unmarshal(bossBytes, &pkg); err != nil {
+ t.Fatalf("Failed to parse boss.json: %v", err)
+ }
+
+ if pkg.Name != projectName {
+ t.Errorf("Expected package name %q, got %q", projectName, pkg.Name)
+ }
+ if pkg.Version != "1.0.0" {
+ t.Errorf("Expected package version '1.0.0', got %q", pkg.Version)
+ }
+ if pkg.MainSrc != "src" {
+ t.Errorf("Expected mainsrc 'src', got %q", pkg.MainSrc)
+ }
+
+ // Check .dpr file
+ dprPath := filepath.Join(projectPath, projectName+".dpr")
+ if _, err := os.Stat(dprPath); os.IsNotExist(err) {
+ t.Fatal(".dpr file was not created")
+ }
+
+ dprBytes, err := os.ReadFile(dprPath)
+ if err != nil {
+ t.Fatalf("Failed to read .dpr file: %v", err)
+ }
+ dprContent := string(dprBytes)
+ if !strings.Contains(dprContent, "program "+projectName) {
+ t.Errorf("Expected .dpr file to contain program declaration")
+ }
+
+ // Check .dproj file
+ dprojPath := filepath.Join(projectPath, projectName+".dproj")
+ if _, err := os.Stat(dprojPath); os.IsNotExist(err) {
+ t.Fatal(".dproj file was not created")
+ }
+
+ dprojBytes, err := os.ReadFile(dprojPath)
+ if err != nil {
+ t.Fatalf("Failed to read .dproj file: %v", err)
+ }
+ dprojContent := string(dprojBytes)
+ if !strings.Contains(dprojContent, "") {
+ t.Error("Expected .dproj to contain ProjectGuid")
+ }
+ if !strings.Contains(dprojContent, "Console") {
+ t.Error("Expected .dproj to have AppType Console")
+ }
+}
+
+// TestDoCreateProject_Pkg tests bootstrapping a package project.
+func TestDoCreateProject_Pkg(t *testing.T) {
+ tempDir := t.TempDir()
+
+ // Redirect working directory
+ oldWd, err := os.Getwd()
+ if err == nil {
+ defer func() { _ = os.Chdir(oldWd) }()
+ }
+ if err := os.Chdir(tempDir); err != nil {
+ t.Fatalf("Failed to change directory: %v", err)
+ }
+
+ // Initialize package manager
+ fs := filesystem.NewOSFileSystem()
+ packageRepo := repository.NewFilePackageRepository(fs)
+ lockRepo := repository.NewFileLockRepository(fs)
+ packageService := packages.NewPackageService(packageRepo, lockRepo)
+ pkgmanager.SetInstance(packageService)
+
+ projectName := "testpkg"
+ doCreateProject(projectName, "pkg", true)
+
+ projectPath := filepath.Join(tempDir, projectName)
+ if _, err := os.Stat(projectPath); os.IsNotExist(err) {
+ t.Fatalf("Project directory was not created")
+ }
+
+ // Check .dpk file
+ dpkPath := filepath.Join(projectPath, projectName+".dpk")
+ if _, err := os.Stat(dpkPath); os.IsNotExist(err) {
+ t.Fatal(".dpk file was not created")
+ }
+
+ dpkBytes, err := os.ReadFile(dpkPath)
+ if err != nil {
+ t.Fatalf("Failed to read .dpk file: %v", err)
+ }
+ dpkContent := string(dpkBytes)
+ if !strings.Contains(dpkContent, "package "+projectName) {
+ t.Errorf("Expected .dpk file to contain package declaration")
+ }
+
+ // Check .dproj file
+ dprojPath := filepath.Join(projectPath, projectName+".dproj")
+ if _, err := os.Stat(dprojPath); os.IsNotExist(err) {
+ t.Fatal(".dproj file was not created")
+ }
+
+ dprojBytes, err := os.ReadFile(dprojPath)
+ if err != nil {
+ t.Fatalf("Failed to read .dproj file: %v", err)
+ }
+ dprojContent := string(dprojBytes)
+ if !strings.Contains(dprojContent, "Package") {
+ t.Error("Expected .dproj to have AppType Package")
+ }
+}
diff --git a/internal/adapters/primary/cli/root.go b/internal/adapters/primary/cli/root.go
index d24d18a7..d4eeaa1f 100644
--- a/internal/adapters/primary/cli/root.go
+++ b/internal/adapters/primary/cli/root.go
@@ -50,6 +50,7 @@ func Execute() error {
config.RegisterConfigCommand(root)
initCmdRegister(root)
+ newCmdRegister(root)
installCmdRegister(root)
loginCmdRegister(root)
runCmdRegister(root)
diff --git a/internal/adapters/secondary/repository/repository_test.go b/internal/adapters/secondary/repository/repository_test.go
index 813a305b..6098cec2 100644
--- a/internal/adapters/secondary/repository/repository_test.go
+++ b/internal/adapters/secondary/repository/repository_test.go
@@ -6,6 +6,7 @@ import (
"errors"
"io"
"os"
+ "path/filepath"
"testing"
"time"
@@ -27,6 +28,7 @@ func NewMockFileSystem() *MockFileSystem {
}
func (m *MockFileSystem) ReadFile(name string) ([]byte, error) {
+ name = filepath.ToSlash(name)
if data, ok := m.files[name]; ok {
return data, nil
}
@@ -34,6 +36,7 @@ func (m *MockFileSystem) ReadFile(name string) ([]byte, error) {
}
func (m *MockFileSystem) WriteFile(name string, data []byte, _ os.FileMode) error {
+ name = filepath.ToSlash(name)
m.files[name] = data
return nil
}
@@ -43,6 +46,7 @@ func (m *MockFileSystem) MkdirAll(_ string, _ os.FileMode) error {
}
func (m *MockFileSystem) Stat(name string) (os.FileInfo, error) {
+ name = filepath.ToSlash(name)
if _, ok := m.files[name]; ok {
//nolint:nilnil // Mock for testing
return nil, nil
@@ -51,6 +55,7 @@ func (m *MockFileSystem) Stat(name string) (os.FileInfo, error) {
}
func (m *MockFileSystem) Remove(name string) error {
+ name = filepath.ToSlash(name)
delete(m.files, name)
return nil
}
@@ -60,6 +65,8 @@ func (m *MockFileSystem) RemoveAll(_ string) error {
}
func (m *MockFileSystem) Rename(oldpath, newpath string) error {
+ oldpath = filepath.ToSlash(oldpath)
+ newpath = filepath.ToSlash(newpath)
if data, ok := m.files[oldpath]; ok {
m.files[newpath] = data
delete(m.files, oldpath)
@@ -82,6 +89,7 @@ func (m *MockFileSystem) Create(_ string) (io.WriteCloser, error) {
}
func (m *MockFileSystem) Exists(name string) bool {
+ name = filepath.ToSlash(name)
_, ok := m.files[name]
return ok
}