diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e08171..2624e0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ All notable changes to ScriptGo will be documented in this file. ## [0.1.0-alpha.6] - 2026-09-23 ### Features +- feat(cli): support direct execution ('scriptgo file.ts', 'scriptgo -e "code"') and 'scg' CLI alias +- feat(pkgmgr): implement scriptgo add to resolve, record, and install dependencies - feat(benchmarks): add JSON operation suites, standard dataset, and native benchmarks (#22) (a893439) ### Performance diff --git a/Makefile b/Makefile index 887a6f3..1a8a604 100644 --- a/Makefile +++ b/Makefile @@ -1,14 +1,16 @@ .PHONY: all build test test-frontend test-parity test-sanitizers audit lint clean release help BINARY_NAME=scriptgo +ALIAS_NAME=scg BUILD_DIR=bin all: build test -## build: Build the scriptgo CLI binary +## build: Build the scriptgo CLI binary and scg alias build: @echo "==> Building $(BINARY_NAME)..." go build -o $(BINARY_NAME) ./cmd/scriptgo + @ln -sf $(BINARY_NAME) $(ALIAS_NAME) ## test: Run full unit and integration test suite test: @@ -51,7 +53,7 @@ release: ## clean: Clean up build artifacts and temporary files clean: @echo "==> Cleaning up build artifacts..." - rm -f $(BINARY_NAME) + rm -f $(BINARY_NAME) $(ALIAS_NAME) rm -rf $(BUILD_DIR) ## help: Display this help message diff --git a/cmd/scg/main.go b/cmd/scg/main.go new file mode 100644 index 0000000..d8a1b01 --- /dev/null +++ b/cmd/scg/main.go @@ -0,0 +1,40 @@ +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" +) + +func main() { + // 1. Look for sibling "scriptgo" in the same directory as this binary + var scriptgoPath string + if selfExe, err := os.Executable(); err == nil { + candidate := filepath.Join(filepath.Dir(selfExe), "scriptgo") + if info, statErr := os.Stat(candidate); statErr == nil && !info.IsDir() { + scriptgoPath = candidate + } + } + // 2. Look in PATH + if scriptgoPath == "" { + if path, lookErr := exec.LookPath("scriptgo"); lookErr == nil { + scriptgoPath = path + } + } + if scriptgoPath == "" { + fmt.Fprintf(os.Stderr, "scg: could not locate 'scriptgo' binary in PATH or executable directory\n") + os.Exit(1) + } + + cmd := exec.Command(scriptgoPath, os.Args[1:]...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + os.Exit(exitErr.ExitCode()) + } + os.Exit(1) + } +} diff --git a/cmd/scriptgo/add_test.go b/cmd/scriptgo/add_test.go new file mode 100644 index 0000000..c859305 --- /dev/null +++ b/cmd/scriptgo/add_test.go @@ -0,0 +1,189 @@ +package main + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "crypto/sha512" + "encoding/base64" + "encoding/json" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" +) + +func createMockTarball(t *testing.T, filename, content string) ([]byte, string) { + t.Helper() + var buf bytes.Buffer + gzw := gzip.NewWriter(&buf) + tw := tar.NewWriter(gzw) + hdr := &tar.Header{ + Name: "package/" + filename, + Mode: 0o644, + Size: int64(len(content)), + } + if err := tw.WriteHeader(hdr); err != nil { + t.Fatal(err) + } + if _, err := tw.Write([]byte(content)); err != nil { + t.Fatal(err) + } + if err := tw.Close(); err != nil { + t.Fatal(err) + } + if err := gzw.Close(); err != nil { + t.Fatal(err) + } + archive := buf.Bytes() + digest := sha512.Sum512(archive) + integrity := "sha512-" + base64.StdEncoding.EncodeToString(digest[:]) + return archive, integrity +} + +func TestCLI_Add(t *testing.T) { + tmpDir := t.TempDir() + binPath := filepath.Join(tmpDir, "scriptgo") + buildCmd := exec.Command("go", "build", "-o", binPath, ".") + if out, err := buildCmd.CombinedOutput(); err != nil { + t.Fatalf("failed to build scriptgo: %v\noutput: %s", err, string(out)) + } + + pkgArchive, integrity := createMockTarball(t, "index.js", "module.exports = 'from-add-cli';\n") + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) { + if request.URL.Path == "/my-pkg" { + tarball := "http://" + request.Host + "/my-pkg/-/my-pkg.tgz" + _ = json.NewEncoder(w).Encode(map[string]any{ + "dist-tags": map[string]string{"latest": "1.5.0"}, + "versions": map[string]any{ + "1.5.0": map[string]any{ + "name": "my-pkg", + "version": "1.5.0", + "dist": map[string]string{ + "tarball": tarball, + "integrity": integrity, + }, + }, + }, + }) + return + } + if request.URL.Path == "/my-pkg/-/my-pkg.tgz" { + _, _ = w.Write(pkgArchive) + return + } + http.NotFound(w, request) + })) + defer server.Close() + + t.Run("no packages specified returns error", func(t *testing.T) { + cmd := exec.Command(binPath, "add") + out, err := cmd.CombinedOutput() + if err == nil { + t.Fatalf("expected error for empty add, got: %s", string(out)) + } + if !strings.Contains(string(out), "at least one package must be specified") { + t.Fatalf("unexpected output: %s", string(out)) + } + }) + + t.Run("mutually exclusive flags return error", func(t *testing.T) { + cmd := exec.Command(binPath, "add", "-D", "-O", "my-pkg") + out, err := cmd.CombinedOutput() + if err == nil { + t.Fatalf("expected error for mutually exclusive flags, got: %s", string(out)) + } + if !strings.Contains(string(out), "mutually exclusive") { + t.Fatalf("unexpected output: %s", string(out)) + } + }) + + t.Run("add package successfully", func(t *testing.T) { + projectDir := filepath.Join(tmpDir, "project1") + if err := os.MkdirAll(projectDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(projectDir, "package.json"), []byte(`{"name":"test1"}`), 0o644); err != nil { + t.Fatal(err) + } + + cmd := exec.Command(binPath, "add", "--project", projectDir, "--registry", server.URL, "my-pkg") + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("add failed: %v\noutput: %s", err, string(out)) + } + if !strings.Contains(string(out), "added my-pkg@^1.5.0") { + t.Fatalf("unexpected stdout: %s", string(out)) + } + + manifestBytes, err := os.ReadFile(filepath.Join(projectDir, "package.json")) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(manifestBytes), `"my-pkg": "^1.5.0"`) { + t.Fatalf("package.json does not contain dependency: %s", string(manifestBytes)) + } + + installedFile, err := os.ReadFile(filepath.Join(projectDir, "node_modules", "my-pkg", "index.js")) + if err != nil { + t.Fatalf("installed file missing: %v", err) + } + if string(installedFile) != "module.exports = 'from-add-cli';\n" { + t.Fatalf("unexpected installed content: %s", string(installedFile)) + } + }) + + t.Run("add dev dependency with -D flag", func(t *testing.T) { + projectDir := filepath.Join(tmpDir, "project2") + if err := os.MkdirAll(projectDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(projectDir, "package.json"), []byte(`{"name":"test2"}`), 0o644); err != nil { + t.Fatal(err) + } + + cmd := exec.Command(binPath, "add", "--project", projectDir, "--registry", server.URL, "-D", "my-pkg") + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("add -D failed: %v\noutput: %s", err, string(out)) + } + + manifestBytes, err := os.ReadFile(filepath.Join(projectDir, "package.json")) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(manifestBytes), `"devDependencies"`) || !strings.Contains(string(manifestBytes), `"my-pkg": "^1.5.0"`) { + t.Fatalf("package.json does not contain devDependency: %s", string(manifestBytes)) + } + }) + + t.Run("add exact version with -E flag", func(t *testing.T) { + projectDir := filepath.Join(tmpDir, "project3") + if err := os.MkdirAll(projectDir, 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(filepath.Join(projectDir, "package.json"), []byte(`{"name":"test3"}`), 0o644); err != nil { + t.Fatal(err) + } + + cmd := exec.Command(binPath, "add", "--project", projectDir, "--registry", server.URL, "-E", "my-pkg@1.5.0") + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("add -E failed: %v\noutput: %s", err, string(out)) + } + if !strings.Contains(string(out), "added my-pkg@1.5.0") { + t.Fatalf("unexpected stdout: %s", string(out)) + } + + manifestBytes, err := os.ReadFile(filepath.Join(projectDir, "package.json")) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(string(manifestBytes), `"my-pkg": "1.5.0"`) { + t.Fatalf("package.json does not contain exact version: %s", string(manifestBytes)) + } + }) +} diff --git a/cmd/scriptgo/coverage_cmd.go b/cmd/scriptgo/coverage_cmd.go new file mode 100644 index 0000000..a2f69e6 --- /dev/null +++ b/cmd/scriptgo/coverage_cmd.go @@ -0,0 +1,76 @@ +package main + +import ( + "flag" + "fmt" + "os" + + "github.com/pilotworks/scriptgo/internal/compiler" +) + +func handleCoverage(args []string) { + fs := flag.NewFlagSet("coverage", flag.ContinueOnError) + fs.Usage = printCoverageUsage + eval := fs.String("e", "", "evaluate inline script string") + format := fs.String("format", "summary", "coverage output format: summary, json") + output := fs.String("o", "", "write output to this path (default: stdout)") + verbose := fs.Bool("v", false, "print analysis stages to stderr") + dynamic := registerDynamicFlag(fs) + if err := fs.Parse(args); err != nil { + if err == flag.ErrHelp { + os.Exit(0) + } + os.Exit(2) + } + + var entryPath string + var cleanup func() + if *eval != "" { + var err error + entryPath, cleanup, err = createInlineSourceFile(*eval) + if err != nil { + fmt.Fprintln(os.Stderr, "scriptgo:", err) + os.Exit(1) + } + defer cleanup() + } else if fs.NArg() == 1 { + var err error + entryPath, cleanup, err = resolveInput(fs.Arg(0)) + if err != nil { + fmt.Fprintln(os.Stderr, "scriptgo:", err) + os.Exit(1) + } + defer cleanup() + } else { + printCoverageUsage() + os.Exit(2) + } + + if *verbose { + fmt.Fprintf(os.Stderr, "scriptgo: analyzing coverage for %s\n", entryPath) + } + options := compiler.BuildOptions{Dynamic: *dynamic} + var result string + var err error + switch *format { + case "summary": + result, err = compiler.CoverageSummary(entryPath, options) + case "json": + result, err = compiler.CoverageReportJSON(entryPath, options) + default: + fmt.Fprintf(os.Stderr, "scriptgo: unsupported coverage format %q (supported: summary, json)\n", *format) + os.Exit(2) + } + if err != nil { + printError(err) + os.Exit(1) + } + if *output == "" { + fmt.Print(result) + return + } + if err := os.WriteFile(*output, []byte(result), 0o644); err != nil { + fmt.Fprintln(os.Stderr, "scriptgo:", err) + os.Exit(1) + } +} diff --git a/cmd/scriptgo/emit_cmd.go b/cmd/scriptgo/emit_cmd.go new file mode 100644 index 0000000..681296e --- /dev/null +++ b/cmd/scriptgo/emit_cmd.go @@ -0,0 +1,98 @@ +package main + +import ( + "flag" + "fmt" + "os" + + "github.com/pilotworks/scriptgo/internal/compiler" +) + +func handleEmit(args []string) { + fs := flag.NewFlagSet("emit", flag.ContinueOnError) + fs.Usage = printEmitUsage + eval := fs.String("e", "", "evaluate inline script string") + mode := fs.String("mode", "llvm-ir", "output mode: llvm-ir, typed-ir") + output := fs.String("o", "", "write output to this path (default: stdout)") + verbose := fs.Bool("v", false, "print compilation stages to stderr") + target := fs.String("target", "", "native target triple (default: $SCRIPTGO_TARGET or native)") + debug := fs.Bool("debug", false, "include native debug metadata") + warnRuntimeCasts := fs.Bool("warn-runtime-casts", false, "warn on runtime checked casts") + strictCasts := fs.Bool("strict-casts", false, "treat cast warnings as errors") + optLevel := fs.String("O", "", "optimization level (0, 1, 2, 3, s, z, fast)") + release := fs.Bool("release", false, "build with release optimizations") + dynamic := registerDynamicFlag(fs) + if err := fs.Parse(args); err != nil { + if err == flag.ErrHelp { + os.Exit(0) + } + os.Exit(2) + } + + var entryPath string + var cleanup func() + + if *eval != "" { + var err error + entryPath, cleanup, err = createInlineSourceFile(*eval) + if err != nil { + fmt.Fprintln(os.Stderr, "scriptgo:", err) + os.Exit(1) + } + defer cleanup() + } else if fs.NArg() == 1 { + var err error + entryPath, cleanup, err = resolveInput(fs.Arg(0)) + if err != nil { + fmt.Fprintln(os.Stderr, "scriptgo:", err) + os.Exit(1) + } + defer cleanup() + } else { + printEmitUsage() + os.Exit(2) + } + + options := compiler.BuildOptions{ + Target: *target, + Debug: *debug, + OptLevel: *optLevel, + Release: *release, + WarnRuntimeCasts: *warnRuntimeCasts, + StrictCasts: *strictCasts, + Dynamic: *dynamic, + } + var result string + var err error + + switch *mode { + case "typed-ir": + if *verbose { + fmt.Fprintf(os.Stderr, "scriptgo: emitting typed IR for %s\n", entryPath) + } + result, err = compiler.DumpIRWithOptions(entryPath, options) + case "llvm-ir": + if *verbose { + fmt.Fprintf(os.Stderr, "scriptgo: emitting LLVM IR for %s\n", entryPath) + } + result, err = compiler.CompileWithOptions(entryPath, options) + default: + fmt.Fprintf(os.Stderr, "scriptgo: unsupported emit mode %q (supported: llvm-ir, typed-ir)\n", *mode) + os.Exit(2) + } + + printCompilerWarnings() + if err != nil { + printError(err) + os.Exit(1) + } + + if *output == "" { + fmt.Print(result) + return + } + if err := os.WriteFile(*output, []byte(result), 0o644); err != nil { + fmt.Fprintln(os.Stderr, "scriptgo:", err) + os.Exit(1) + } +} diff --git a/cmd/scriptgo/main.go b/cmd/scriptgo/main.go index 4215875..b597521 100644 --- a/cmd/scriptgo/main.go +++ b/cmd/scriptgo/main.go @@ -21,6 +21,34 @@ func init() { } } +func getProgramName() string { + if len(os.Args) > 0 { + base := filepath.Base(os.Args[0]) + if base == "scg" || base == "scg.exe" { + return "scg" + } + } + return "scriptgo" +} + +func shouldImplicitRun(firstArg string) bool { + if strings.HasPrefix(firstArg, "-") { + return true + } + ext := strings.ToLower(filepath.Ext(firstArg)) + switch ext { + case ".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", ".mjs", ".cjs": + return true + } + if info, err := os.Stat(firstArg); err == nil && !info.IsDir() { + return true + } + if _, err := pkgmgr.ResolveTask(".", "", firstArg); err == nil { + return true + } + return false +} + func main() { if len(os.Args) < 2 { printMainUsage() @@ -41,12 +69,14 @@ func main() { handleCoverage(normalizeFlagsFirst(os.Args[2:])) case "init": handleInit(normalizeFlagsFirst(os.Args[2:])) + case "add": + handleAdd(normalizeFlagsFirstForAdd(os.Args[2:])) case "install": handleInstall(normalizeFlagsFirst(os.Args[2:])) case "task": handleTask(os.Args[2:]) case "version", "--version", "-V": - fmt.Printf("scriptgo version %s (runtime %s)\n", compiler.Version, compiler.RuntimeABIVersion) + fmt.Printf("%s version %s (runtime %s)\n", getProgramName(), compiler.Version, compiler.RuntimeABIVersion) case "help", "--help", "-h": if len(os.Args) > 2 { handleHelpCommand(os.Args[2]) @@ -54,7 +84,11 @@ func main() { } printMainUsage() default: - fmt.Fprintf(os.Stderr, "scriptgo: unknown command %q\n\n", firstArg) + if shouldImplicitRun(firstArg) { + handleRun(normalizeFlagsFirst(os.Args[1:])) + return + } + fmt.Fprintf(os.Stderr, "%s: unknown command %q\n\n", getProgramName(), firstArg) printMainUsage() os.Exit(2) } @@ -74,14 +108,16 @@ func handleHelpCommand(cmd string) { printCoverageUsage() case "init": printInitUsage() + case "add": + printAddUsage() case "install": printInstallUsage() case "task": printTaskUsage() case "version": - fmt.Println("Usage: scriptgo version\n\nPrints the current compiler version and runtime ABI version.") + fmt.Printf("Usage: %s version\n\nPrints the current compiler version and runtime ABI version.\n", getProgramName()) default: - fmt.Fprintf(os.Stderr, "scriptgo: unknown command %q for help\n\n", cmd) + fmt.Fprintf(os.Stderr, "%s: unknown command %q for help\n\n", getProgramName(), cmd) printMainUsage() os.Exit(2) } @@ -151,6 +187,7 @@ func handleRun(args []string) { fs := flag.NewFlagSet("run", flag.ContinueOnError) fs.Usage = printRunUsage eval := fs.String("e", "", "evaluate inline script string") + fs.StringVar(eval, "eval", "", "evaluate inline script string") verbose := fs.Bool("v", false, "print compilation stages to stderr") target := fs.String("target", "", "native target triple (default: $SCRIPTGO_TARGET or native)") cc := fs.String("cc", "", "C compiler / toolchain driver (default: $SCRIPTGO_CC or clang)") @@ -178,16 +215,24 @@ func handleRun(args []string) { os.Exit(2) } + hasEval := false + for _, a := range args { + if a == "-e" || a == "--eval" || strings.HasPrefix(a, "-e=") || strings.HasPrefix(a, "--eval=") { + hasEval = true + break + } + } + var entryPath string var cleanup func() var extraArgs []string var extraSources []string - if *eval != "" { + if *eval != "" || hasEval { var err error entryPath, cleanup, err = createInlineSourceFile(*eval) if err != nil { - fmt.Fprintf(os.Stderr, "scriptgo: %v\n", err) + fmt.Fprintf(os.Stderr, "%s: %v\n", getProgramName(), err) os.Exit(1) } defer cleanup() @@ -201,7 +246,7 @@ func handleRun(args []string) { var err error entryPath, cleanup, err = resolveInput(firstArg) if err != nil { - fmt.Fprintf(os.Stderr, "scriptgo: %v\n", err) + fmt.Fprintf(os.Stderr, "%s: %v\n", getProgramName(), err) os.Exit(1) } defer cleanup() @@ -577,162 +622,6 @@ func handleCheck(args []string) { } } -func handleEmit(args []string) { - fs := flag.NewFlagSet("emit", flag.ContinueOnError) - fs.Usage = printEmitUsage - eval := fs.String("e", "", "evaluate inline script string") - mode := fs.String("mode", "llvm-ir", "output mode: llvm-ir, typed-ir") - output := fs.String("o", "", "write output to this path (default: stdout)") - verbose := fs.Bool("v", false, "print compilation stages to stderr") - target := fs.String("target", "", "native target triple (default: $SCRIPTGO_TARGET or native)") - debug := fs.Bool("debug", false, "include native debug metadata") - warnRuntimeCasts := fs.Bool("warn-runtime-casts", false, "warn on runtime checked casts") - strictCasts := fs.Bool("strict-casts", false, "treat cast warnings as errors") - optLevel := fs.String("O", "", "optimization level (0, 1, 2, 3, s, z, fast)") - release := fs.Bool("release", false, "build with release optimizations") - dynamic := registerDynamicFlag(fs) - if err := fs.Parse(args); err != nil { - if err == flag.ErrHelp { - os.Exit(0) - } - os.Exit(2) - } - - var entryPath string - var cleanup func() - - if *eval != "" { - var err error - entryPath, cleanup, err = createInlineSourceFile(*eval) - if err != nil { - fmt.Fprintln(os.Stderr, "scriptgo:", err) - os.Exit(1) - } - defer cleanup() - } else if fs.NArg() == 1 { - var err error - entryPath, cleanup, err = resolveInput(fs.Arg(0)) - if err != nil { - fmt.Fprintln(os.Stderr, "scriptgo:", err) - os.Exit(1) - } - defer cleanup() - } else { - printEmitUsage() - os.Exit(2) - } - - options := compiler.BuildOptions{ - Target: *target, - Debug: *debug, - OptLevel: *optLevel, - Release: *release, - WarnRuntimeCasts: *warnRuntimeCasts, - StrictCasts: *strictCasts, - Dynamic: *dynamic, - } - var result string - var err error - - switch *mode { - case "typed-ir": - if *verbose { - fmt.Fprintf(os.Stderr, "scriptgo: emitting typed IR for %s\n", entryPath) - } - result, err = compiler.DumpIRWithOptions(entryPath, options) - case "llvm-ir": - if *verbose { - fmt.Fprintf(os.Stderr, "scriptgo: emitting LLVM IR for %s\n", entryPath) - } - result, err = compiler.CompileWithOptions(entryPath, options) - default: - fmt.Fprintf(os.Stderr, "scriptgo: unsupported emit mode %q (supported: llvm-ir, typed-ir)\n", *mode) - os.Exit(2) - } - - printCompilerWarnings() - if err != nil { - printError(err) - os.Exit(1) - } - - if *output == "" { - fmt.Print(result) - return - } - if err := os.WriteFile(*output, []byte(result), 0o644); err != nil { - fmt.Fprintln(os.Stderr, "scriptgo:", err) - os.Exit(1) - } -} - -func handleCoverage(args []string) { - fs := flag.NewFlagSet("coverage", flag.ContinueOnError) - fs.Usage = printCoverageUsage - eval := fs.String("e", "", "evaluate inline script string") - format := fs.String("format", "summary", "coverage output format: summary, json") - output := fs.String("o", "", "write output to this path (default: stdout)") - verbose := fs.Bool("v", false, "print analysis stages to stderr") - dynamic := registerDynamicFlag(fs) - if err := fs.Parse(args); err != nil { - if err == flag.ErrHelp { - os.Exit(0) - } - os.Exit(2) - } - - var entryPath string - var cleanup func() - if *eval != "" { - var err error - entryPath, cleanup, err = createInlineSourceFile(*eval) - if err != nil { - fmt.Fprintln(os.Stderr, "scriptgo:", err) - os.Exit(1) - } - defer cleanup() - } else if fs.NArg() == 1 { - var err error - entryPath, cleanup, err = resolveInput(fs.Arg(0)) - if err != nil { - fmt.Fprintln(os.Stderr, "scriptgo:", err) - os.Exit(1) - } - defer cleanup() - } else { - printCoverageUsage() - os.Exit(2) - } - - if *verbose { - fmt.Fprintf(os.Stderr, "scriptgo: analyzing coverage for %s\n", entryPath) - } - options := compiler.BuildOptions{Dynamic: *dynamic} - var result string - var err error - switch *format { - case "summary": - result, err = compiler.CoverageSummary(entryPath, options) - case "json": - result, err = compiler.CoverageReportJSON(entryPath, options) - default: - fmt.Fprintf(os.Stderr, "scriptgo: unsupported coverage format %q (supported: summary, json)\n", *format) - os.Exit(2) - } - if err != nil { - printError(err) - os.Exit(1) - } - if *output == "" { - fmt.Print(result) - return - } - if err := os.WriteFile(*output, []byte(result), 0o644); err != nil { - fmt.Fprintln(os.Stderr, "scriptgo:", err) - os.Exit(1) - } -} - func printCompilerWarnings() { warns := compiler.GetWarnings() for _, w := range warns { diff --git a/cmd/scriptgo/main_test.go b/cmd/scriptgo/main_test.go index e5ee038..91d0827 100644 --- a/cmd/scriptgo/main_test.go +++ b/cmd/scriptgo/main_test.go @@ -201,3 +201,73 @@ func TestCLI_CheckTSConfig(t *testing.T) { } }) } + +func TestCLI_DirectExecutionAndAlias(t *testing.T) { + tmpDir := t.TempDir() + binPath := testScriptGoBin + + tsFile := filepath.Join(tmpDir, "hello.ts") + if err := os.WriteFile(tsFile, []byte("console.log('direct_file_works');\n"), 0o644); err != nil { + t.Fatal(err) + } + + t.Run("scriptgo -e code", func(t *testing.T) { + cmd := exec.Command(binPath, "-e", "console.log('direct_eval_works');") + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("expected scriptgo -e to succeed: %v\noutput: %s", err, string(out)) + } + if !strings.Contains(string(out), "direct_eval_works") { + t.Errorf("unexpected output: %s", string(out)) + } + }) + + t.Run("scriptgo file.ts", func(t *testing.T) { + cmd := exec.Command(binPath, tsFile) + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("expected scriptgo file.ts to succeed: %v\noutput: %s", err, string(out)) + } + if !strings.Contains(string(out), "direct_file_works") { + t.Errorf("unexpected output: %s", string(out)) + } + }) + + scgBin := filepath.Join(tmpDir, "scg") + if err := os.Symlink(binPath, scgBin); err != nil { + t.Skipf("symlink not supported on this platform: %v", err) + } + + t.Run("scg -e code", func(t *testing.T) { + cmd := exec.Command(scgBin, "-e", "console.log('scg_eval_works');") + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("expected scg -e to succeed: %v\noutput: %s", err, string(out)) + } + if !strings.Contains(string(out), "scg_eval_works") { + t.Errorf("unexpected output: %s", string(out)) + } + }) + + t.Run("scg file.ts", func(t *testing.T) { + cmd := exec.Command(scgBin, tsFile) + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("expected scg file.ts to succeed: %v\noutput: %s", err, string(out)) + } + if !strings.Contains(string(out), "direct_file_works") { + t.Errorf("unexpected output: %s", string(out)) + } + }) + + t.Run("scg version", func(t *testing.T) { + cmd := exec.Command(scgBin, "version") + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("expected scg version to succeed: %v\noutput: %s", err, string(out)) + } + if !strings.Contains(string(out), "scg version") { + t.Errorf("unexpected output: %s", string(out)) + } + }) +} diff --git a/cmd/scriptgo/package_add.go b/cmd/scriptgo/package_add.go new file mode 100644 index 0000000..a9cb39b --- /dev/null +++ b/cmd/scriptgo/package_add.go @@ -0,0 +1,121 @@ +package main + +import ( + "flag" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/pilotworks/scriptgo/internal/pkgmgr" +) + +func handleAdd(args []string) { + fs := flag.NewFlagSet("add", flag.ContinueOnError) + fs.Usage = printAddUsage + project := fs.String("project", ".", "project directory containing package.json") + manifest := fs.String("manifest", "", "package manifest path (default: /package.json)") + lockfile := fs.String("lockfile", "", "lockfile path (default: /scriptgo-lock.json)") + store := fs.String("store", "", "content store path (default: /.scriptgo/store)") + registry := fs.String("registry", "", "npm-compatible registry URL") + token := fs.String("registry-token", "", "registry bearer token (default: SCRIPTGO_NPM_TOKEN or NPM_TOKEN)") + dev := fs.Bool("dev", false, "save package to devDependencies") + fs.BoolVar(dev, "D", false, "save package to devDependencies (shorthand)") + optional := fs.Bool("optional", false, "save package to optionalDependencies") + fs.BoolVar(optional, "O", false, "save package to optionalDependencies (shorthand)") + peer := fs.Bool("peer", false, "save package to peerDependencies") + exact := fs.Bool("exact", false, "save exact version instead of ^x.y.z") + fs.BoolVar(exact, "E", false, "save exact version instead of ^x.y.z (shorthand)") + offline := fs.Bool("offline", false, "install only from the existing lockfile and content store") + + if err := fs.Parse(args); err != nil { + if err == flag.ErrHelp { + os.Exit(0) + } + os.Exit(2) + } + + depCount := 0 + if *dev { + depCount++ + } + if *optional { + depCount++ + } + if *peer { + depCount++ + } + if depCount > 1 { + fmt.Fprintf(os.Stderr, "scriptgo add: --dev, --optional, and --peer are mutually exclusive\n") + os.Exit(2) + } + + packages := fs.Args() + if len(packages) == 0 { + fmt.Fprintf(os.Stderr, "scriptgo add: at least one package must be specified\n\n") + printAddUsage() + os.Exit(2) + } + + depType := pkgmgr.DepTypeRegular + if *dev { + depType = pkgmgr.DepTypeDev + } else if *optional { + depType = pkgmgr.DepTypeOptional + } else if *peer { + depType = pkgmgr.DepTypePeer + } + + root, err := filepath.Abs(*project) + if err != nil { + fmt.Fprintf(os.Stderr, "scriptgo: %v\n", err) + os.Exit(1) + } + + result, err := pkgmgr.Add(pkgmgr.AddOptions{ + ProjectRoot: root, + Manifest: *manifest, + Lockfile: *lockfile, + StoreRoot: *store, + Registry: pkgmgr.Registry{BaseURL: *registry, Token: *token}, + Dependencies: packages, + DepType: depType, + Exact: *exact, + Offline: *offline, + }) + if err != nil { + fmt.Fprintf(os.Stderr, "scriptgo add: %v\n", err) + os.Exit(1) + } + + if len(result.Added) == 1 { + for name, spec := range result.Added { + fmt.Fprintf(os.Stdout, "added %s@%s, installed %d packages\n", name, spec, len(result.Lockfile.Packages)) + } + } else { + fmt.Fprintf(os.Stdout, "added %d packages, installed %d packages\n", len(result.Added), len(result.Lockfile.Packages)) + } +} + +// normalizeFlagsFirstForAdd moves flags to the front so that positional packages +// can appear before, after, or between flags. +func normalizeFlagsFirstForAdd(args []string) []string { + var flags []string + var positionals []string + for i := 0; i < len(args); i++ { + arg := args[i] + if strings.HasPrefix(arg, "-") { + flags = append(flags, arg) + if (arg == "-project" || arg == "--project" || arg == "-manifest" || arg == "--manifest" || + arg == "-lockfile" || arg == "--lockfile" || arg == "-store" || arg == "--store" || + arg == "-registry" || arg == "--registry" || arg == "-registry-token" || arg == "--registry-token") && + i+1 < len(args) && !strings.HasPrefix(args[i+1], "-") { + i++ + flags = append(flags, args[i]) + } + } else { + positionals = append(positionals, arg) + } + } + return append(flags, positionals...) +} diff --git a/cmd/scriptgo/usage.go b/cmd/scriptgo/usage.go index e22ce8b..d943f5a 100644 --- a/cmd/scriptgo/usage.go +++ b/cmd/scriptgo/usage.go @@ -6,10 +6,16 @@ import ( ) func printMainUsage() { - fmt.Fprintln(os.Stderr, `ScriptGo - TypeScript Native Compiler + prog := getProgramName() + fmt.Fprintf(os.Stderr, `ScriptGo - TypeScript Native Compiler Usage: - scriptgo [flags] + %[1]s [flags] [-- ] + %[1]s [flags] -e "" [-- ] + %[1]s [flags] + +Alias: + scg (can be used interchangeably with scriptgo) Commands: run Run a package.json script or compile and execute a TypeScript program @@ -18,10 +24,11 @@ Commands: emit Emit LLVM IR or Typed IR coverage Analyze Static/Dynamic site coverage init Initialize a new ScriptGo TypeScript project + add Add dependencies to package.json and install them install Resolve, verify, cache, and link package.json dependencies task Run a package.json script with node_modules/.bin in PATH version Print compiler and runtime ABI version - help Show help for ScriptGo or a specific command + help Show help for %[1]s or a specific command Global Flags: -v Verbose output @@ -45,7 +52,7 @@ Global Flags: --store Content store path with --install -h, --help Show help message -Use 'scriptgo help ' or 'scriptgo --help' for detailed command usage.`) +Use '%[1]s help ' or '%[1]s --help' for detailed command usage.`, prog) } func printTaskUsage() { @@ -89,6 +96,37 @@ Examples: scriptgo init -f`) } +func printAddUsage() { + fmt.Fprintln(os.Stderr, `Usage: + scriptgo add [flags] + +Description: + Resolves package versions from an npm-compatible registry or local workspace, + records them in package.json, and installs them into node_modules and + scriptgo-lock.json. + +Flags: + --project Project directory containing package.json (default: .) + --manifest package.json path (default: /package.json) + --lockfile Lockfile path (default: /scriptgo-lock.json) + --store Content store path (default: /.scriptgo/store) + --registry npm-compatible registry URL + --registry-token registry bearer token (or SCRIPTGO_NPM_TOKEN/NPM_TOKEN) + -D, --dev Add to devDependencies + -O, --optional Add to optionalDependencies + --peer Add to peerDependencies + -E, --exact Save exact version instead of ^x.y.z + --offline Use only cached tarballs and lockfile + -h, --help Show this help message + +Examples: + scriptgo add lodash + scriptgo add chalk@4.1.2 + scriptgo add -D typescript @types/node + scriptgo add -E express + scriptgo add --optional redis`) +} + func printInstallUsage() { fmt.Fprintln(os.Stderr, `Usage: scriptgo install [flags] @@ -111,10 +149,13 @@ Flags: } func printRunUsage() { - fmt.Fprintln(os.Stderr, `Usage: - scriptgo run [