From 051153bf917b563ae93e02a47444ebc6cee36f42 Mon Sep 17 00:00:00 2001 From: im10furry Date: Sun, 5 Jul 2026 15:09:10 -0500 Subject: [PATCH] feat: improve CLI usability --- README.md | 24 +++-- README_zh.md | 24 +++-- docs/guide/getting-started.md | 16 +-- docs/zh/guide/getting-started.md | 16 +-- internal/cli/cli.go | 180 ++++++++++++++++++++++++++----- internal/cli/cli_test.go | 97 +++++++++++++++++ 6 files changed, 299 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index 730763b..1acb599 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ go install github.com/DarkInno/scion/cmd/scion@latest For reproducible installs, pin a release: ```bash -go install github.com/DarkInno/scion/cmd/scion@v0.1.2 +go install github.com/DarkInno/scion/cmd/scion@v0.1.3 ``` Make sure your Go bin directory is on `PATH`, then verify the install: @@ -30,15 +30,17 @@ scion list Copy a standard-library-only module into your project: ```bash -scion add cache --to internal/cache --dry-run -scion add cache --to internal/cache -scion diff cache --target internal/cache +scion add cache --dry-run +scion add cache +scion diff cache ``` +When `--to` or `--target` is omitted, Scion uses the module's default target, such as `internal/cache`. You can still override it with `--to ` or `--target `. + Scion copies source files and writes `.scion-module.json` metadata for later comparison. It never edits your project's `go.mod` automatically. Modules marked `stdlibOnly=false`, such as `auth`, require explicit standalone mode: ```bash -scion add auth --standalone --to internal/auth +scion add auth --standalone ``` ## Binary Downloads @@ -54,7 +56,7 @@ sha256sum -c SHA256SUMS On Windows PowerShell: ```powershell -Get-FileHash .\scion_v0.1.2_windows_amd64.zip -Algorithm SHA256 +Get-FileHash .\scion_v0.1.3_windows_amd64.zip -Algorithm SHA256 ``` ## Why Copy-Paste? @@ -87,12 +89,14 @@ Backend modules such as auth, CRUD, file upload, and rate limiting share most of ```bash scion list [--json] scion info [--json] -scion add --to [--dry-run] [--force] [--standalone] -scion diff --target [--json] +scion add [--to ] [--dry-run] [--force] [--standalone] +scion diff [--target ] [--json] scion doctor [--strict] [--json] scion version [--json] ``` +Use `scion help ` for command-specific examples. + ## Project Structure ```text @@ -148,8 +152,8 @@ foreach ($m in $modules) { Push-Location "registry/$m/src/go"; go test ./...; Po Releases are created from semantic version tags: ```bash -git tag -a v0.1.2 -m "v0.1.2" -git push origin v0.1.2 +git tag -a v0.1.3 -m "v0.1.3" +git push origin v0.1.3 ``` The release workflow verifies the CLI, rebuilds the embedded bundle check, cross-compiles binaries, generates `SHA256SUMS`, and publishes GitHub Release assets. diff --git a/README_zh.md b/README_zh.md index 73559c4..c82108d 100644 --- a/README_zh.md +++ b/README_zh.md @@ -17,7 +17,7 @@ go install github.com/DarkInno/scion/cmd/scion@latest 如果需要固定版本: ```bash -go install github.com/DarkInno/scion/cmd/scion@v0.1.2 +go install github.com/DarkInno/scion/cmd/scion@v0.1.3 ``` 确认 Go bin 目录已加入 `PATH`,然后验证安装: @@ -30,15 +30,17 @@ scion list 复制一个仅使用标准库的模块: ```bash -scion add cache --to internal/cache --dry-run -scion add cache --to internal/cache -scion diff cache --target internal/cache +scion add cache --dry-run +scion add cache +scion diff cache ``` +省略 `--to` 或 `--target` 时,Scion 会使用模块的默认目标目录,例如 `internal/cache`。你仍然可以用 `--to ` 或 `--target ` 覆盖。 + Scion CLI 会复制源码,并写入 `.scion-module.json` 供后续对比使用。它不会自动修改你的 `go.mod`。标记为 `stdlibOnly=false` 的模块,例如 `auth`,需要显式使用 standalone 模式: ```bash -scion add auth --standalone --to internal/auth +scion add auth --standalone ``` ## 二进制下载 @@ -54,7 +56,7 @@ sha256sum -c SHA256SUMS Windows PowerShell: ```powershell -Get-FileHash .\scion_v0.1.2_windows_amd64.zip -Algorithm SHA256 +Get-FileHash .\scion_v0.1.3_windows_amd64.zip -Algorithm SHA256 ``` ## 为什么是复制粘贴? @@ -87,12 +89,14 @@ Get-FileHash .\scion_v0.1.2_windows_amd64.zip -Algorithm SHA256 ```bash scion list [--json] scion info [--json] -scion add --to [--dry-run] [--force] [--standalone] -scion diff --target [--json] +scion add [--to ] [--dry-run] [--force] [--standalone] +scion diff [--target ] [--json] scion doctor [--strict] [--json] scion version [--json] ``` +使用 `scion help ` 查看某个命令的示例和选项。 + ## 项目结构 ```text @@ -148,8 +152,8 @@ foreach ($m in $modules) { Push-Location "registry/$m/src/go"; go test ./...; Po 通过语义化版本 tag 发布: ```bash -git tag -a v0.1.2 -m "v0.1.2" -git push origin v0.1.2 +git tag -a v0.1.3 -m "v0.1.3" +git push origin v0.1.3 ``` Release workflow 会验证 CLI、检查内置 bundle、交叉编译二进制、生成 `SHA256SUMS`,并发布 GitHub Release 资产。 diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index dd814ed..f218c29 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -11,7 +11,7 @@ go install github.com/DarkInno/scion/cmd/scion@latest For reproducible installs, pin a release: ```bash -go install github.com/DarkInno/scion/cmd/scion@v0.1.2 +go install github.com/DarkInno/scion/cmd/scion@v0.1.3 ``` Make sure your Go bin directory is on `PATH`, then verify the install: @@ -37,15 +37,17 @@ Modules marked `stdlibOnly=true` can be copied directly into an existing project Preview the files first: ```bash -scion add cache --to internal/cache --dry-run +scion add cache --dry-run ``` Copy the module: ```bash -scion add cache --to internal/cache +scion add cache ``` +Scion uses the module's default target when `--to` is omitted, such as `internal/cache` for `cache`. You can override it with `--to `. + Scion writes `.scion-module.json` metadata in the target directory. This file records the copied module, registry version, source hashes, and whether standalone mode was used. ## 4. Compare Later @@ -53,17 +55,17 @@ Scion writes `.scion-module.json` metadata in the target directory. This file re After you adapt the copied source, you can compare it against Scion's embedded template: ```bash -scion diff cache --target internal/cache +scion diff cache ``` -`diff` only reports differences. It never merges or overwrites your local changes. +`diff` uses the module's default target when `--target` is omitted. It only reports differences and never merges or overwrites your local changes. ## Standalone Modules The `auth` module intentionally uses mature security dependencies for JWT and bcrypt. Copy it with standalone mode: ```bash -scion add auth --standalone --to internal/auth +scion add auth --standalone ``` Scion still does not edit your project-level `go.mod`; standalone mode only copies the module's own `go.mod` and `go.sum` into the target. @@ -81,7 +83,7 @@ sha256sum -c SHA256SUMS Windows PowerShell: ```powershell -Get-FileHash .\scion_v0.1.2_windows_amd64.zip -Algorithm SHA256 +Get-FileHash .\scion_v0.1.3_windows_amd64.zip -Algorithm SHA256 ``` ## Manual Copy diff --git a/docs/zh/guide/getting-started.md b/docs/zh/guide/getting-started.md index 1e8e37e..6c5653c 100644 --- a/docs/zh/guide/getting-started.md +++ b/docs/zh/guide/getting-started.md @@ -11,7 +11,7 @@ go install github.com/DarkInno/scion/cmd/scion@latest 如果需要固定版本: ```bash -go install github.com/DarkInno/scion/cmd/scion@v0.1.2 +go install github.com/DarkInno/scion/cmd/scion@v0.1.3 ``` 确认 Go bin 目录已加入 `PATH`,然后验证安装: @@ -37,15 +37,17 @@ scion info cache 先预览将要复制的文件: ```bash -scion add cache --to internal/cache --dry-run +scion add cache --dry-run ``` 执行复制: ```bash -scion add cache --to internal/cache +scion add cache ``` +省略 `--to` 时,Scion 会使用模块默认目标目录,例如 `cache` 的默认目标是 `internal/cache`。你可以用 `--to ` 覆盖。 + Scion 会在目标目录写入 `.scion-module.json`。这个文件记录模块名、registry 版本、源文件 hash,以及是否使用 standalone 模式。 ## 4. 后续对比 @@ -53,17 +55,17 @@ Scion 会在目标目录写入 `.scion-module.json`。这个文件记录模块 当你改造过复制进项目的源码后,可以和 Scion 内置模板对比: ```bash -scion diff cache --target internal/cache +scion diff cache ``` -`diff` 只报告差异,不会自动合并,也不会覆盖你的本地改动。 +省略 `--target` 时,`diff` 会使用模块默认目标目录。它只报告差异,不会自动合并,也不会覆盖你的本地改动。 ## Standalone 模块 `auth` 模块为了 JWT 和 bcrypt 使用了成熟安全依赖。复制它时需要 standalone 模式: ```bash -scion add auth --standalone --to internal/auth +scion add auth --standalone ``` Scion 仍然不会修改你的项目级 `go.mod`;standalone 模式只会把该模块自己的 `go.mod` 和 `go.sum` 复制到目标目录。 @@ -81,7 +83,7 @@ sha256sum -c SHA256SUMS Windows PowerShell: ```powershell -Get-FileHash .\scion_v0.1.2_windows_amd64.zip -Algorithm SHA256 +Get-FileHash .\scion_v0.1.3_windows_amd64.zip -Algorithm SHA256 ``` ## 手动复制 diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 28ddf27..8126e53 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -46,9 +46,20 @@ func (a *App) Run(ctx context.Context, args []string, stdout, stderr io.Writer) return a.runDoctor(args[1:], stdout, stderr) case "version": return a.runVersion(args[1:], stdout, stderr) - case "-h", "--help", "help": + case "-h", "--help": writeUsage(stdout) return 0 + case "help": + if len(args) == 1 { + writeUsage(stdout) + return 0 + } + if writeCommandUsage(stdout, args[1]) { + return 0 + } + _, _ = fmt.Fprintf(stderr, "unknown help topic: %s\n\n", args[1]) + writeUsage(stderr) + return 1 default: _, _ = fmt.Fprintf(stderr, "unknown command: %s\n\n", args[0]) writeUsage(stderr) @@ -57,6 +68,10 @@ func (a *App) Run(ctx context.Context, args []string, stdout, stderr io.Writer) } func (a *App) runList(args []string, stdout, stderr io.Writer) int { + if hasHelp(args) { + writeCommandUsage(stdout, "list") + return 0 + } fs := newFlagSet("list", stderr) jsonOut := fs.Bool("json", false, "write JSON") if err := fs.Parse(args); err != nil { @@ -83,6 +98,10 @@ func (a *App) runList(args []string, stdout, stderr io.Writer) int { } func (a *App) runInfo(args []string, stdout, stderr io.Writer) int { + if hasHelp(args) { + writeCommandUsage(stdout, "info") + return 0 + } moduleID, rest, err := takeFirstPositional(args) if err != nil { _, _ = fmt.Fprintf(stderr, "info requires a module id\n") @@ -117,11 +136,19 @@ func (a *App) runInfo(args []string, stdout, stderr io.Writer) int { _, _ = fmt.Fprintf(stdout, "Default target: %s\n", module.DefaultTarget) _, _ = fmt.Fprintf(stdout, "Standard library only: %t\n", module.StdlibOnly) _, _ = fmt.Fprintf(stdout, "Status: %s\n", module.Status) + if !module.StdlibOnly { + _, _ = fmt.Fprintf(stdout, "Standalone required: true\n") + _, _ = fmt.Fprintf(stdout, "Add command: scion add %s --standalone\n", module.ID) + } return 0 } func (a *App) runAdd(args []string, stdout, stderr io.Writer) int { - moduleID, rest, err := takeFirstPositional(args) + if hasHelp(args) { + writeCommandUsage(stdout, "add") + return 0 + } + moduleID, rest, err := takeFirstPositional(args, "to") if err != nil { _, _ = fmt.Fprintf(stderr, "add requires a module id\n") return 1 @@ -138,18 +165,22 @@ func (a *App) runAdd(args []string, stdout, stderr io.Writer) int { _, _ = fmt.Fprintf(stderr, "unexpected argument: %s\n", fs.Arg(0)) return 1 } - if strings.TrimSpace(*target) == "" { - _, _ = fmt.Fprintf(stderr, "add requires --to \n") - return 1 - } module, ok := a.reg.Index.Module(moduleID) if !ok { _, _ = fmt.Fprintf(stderr, "unknown module: %s\n", moduleID) return 1 } + targetDir := strings.TrimSpace(*target) + if targetDir == "" { + targetDir = module.DefaultTarget + } + if targetDir == "" { + _, _ = fmt.Fprintf(stderr, "add requires --to ; module %s has no default target\n", module.ID) + return 1 + } if !module.StdlibOnly && !*standalone { - _, _ = fmt.Fprintf(stderr, "module %s is marked stdlibOnly=false; use --standalone to copy its go.mod/go.sum or choose a zero-dependency module\n", module.ID) + _, _ = fmt.Fprintf(stderr, "module %s is marked stdlibOnly=false; use --standalone to copy its go.mod/go.sum explicitly\n", module.ID) return 1 } @@ -190,16 +221,16 @@ func (a *App) runAdd(args []string, stdout, stderr io.Writer) int { metaData = append(metaData, '\n') copyFiles = append(copyFiles, copytree.File{RelPath: metadataFile, Data: metaData, Mode: 0o644}) - result, err := copytree.CopyFiles(*target, copyFiles, copytree.Options{DryRun: *dryRun, Force: *force}) + result, err := copytree.CopyFiles(targetDir, copyFiles, copytree.Options{DryRun: *dryRun, Force: *force}) if err != nil { _, _ = fmt.Fprintf(stderr, "copy failed: %v\n", err) return 1 } if *dryRun { - _, _ = fmt.Fprintf(stdout, "Would copy %d files for %s to %s\n", len(result.Files), module.ID, *target) + _, _ = fmt.Fprintf(stdout, "Would copy %d files for %s to %s\n", len(result.Files), module.ID, targetDir) } else { - _, _ = fmt.Fprintf(stdout, "Copied %s %s to %s\n", module.ID, module.Version, *target) + _, _ = fmt.Fprintf(stdout, "Copied %s %s to %s\n", module.ID, module.Version, targetDir) } for _, file := range result.Files { _, _ = fmt.Fprintf(stdout, " %s\n", file) @@ -208,7 +239,11 @@ func (a *App) runAdd(args []string, stdout, stderr io.Writer) int { } func (a *App) runDiff(args []string, stdout, stderr io.Writer) int { - moduleID, rest, err := takeFirstPositional(args) + if hasHelp(args) { + writeCommandUsage(stdout, "diff") + return 0 + } + moduleID, rest, err := takeFirstPositional(args, "target") if err != nil { _, _ = fmt.Fprintf(stderr, "diff requires a module id\n") return 1 @@ -223,17 +258,21 @@ func (a *App) runDiff(args []string, stdout, stderr io.Writer) int { _, _ = fmt.Fprintf(stderr, "unexpected argument: %s\n", fs.Arg(0)) return 1 } - if strings.TrimSpace(*target) == "" { - _, _ = fmt.Fprintf(stderr, "diff requires --target \n") - return 1 - } module, ok := a.reg.Index.Module(moduleID) if !ok { _, _ = fmt.Fprintf(stderr, "unknown module: %s\n", moduleID) return 1 } - result, err := buildDiff(a.reg, module, filepath.Clean(*target)) + targetDir := strings.TrimSpace(*target) + if targetDir == "" { + targetDir = module.DefaultTarget + } + if targetDir == "" { + _, _ = fmt.Fprintf(stderr, "diff requires --target ; module %s has no default target\n", module.ID) + return 1 + } + result, err := buildDiff(a.reg, module, filepath.Clean(targetDir)) if err != nil { _, _ = fmt.Fprintf(stderr, "diff failed: %v\n", err) return 1 @@ -250,6 +289,10 @@ func (a *App) runDiff(args []string, stdout, stderr io.Writer) int { } func (a *App) runDoctor(args []string, stdout, stderr io.Writer) int { + if hasHelp(args) { + writeCommandUsage(stdout, "doctor") + return 0 + } fs := newFlagSet("doctor", stderr) strict := fs.Bool("strict", false, "treat release-gate warnings as errors") jsonOut := fs.Bool("json", false, "write JSON") @@ -279,6 +322,10 @@ func (a *App) runDoctor(args []string, stdout, stderr io.Writer) int { } func (a *App) runVersion(args []string, stdout, stderr io.Writer) int { + if hasHelp(args) { + writeCommandUsage(stdout, "version") + return 0 + } fs := newFlagSet("version", stderr) jsonOut := fs.Bool("json", false, "write JSON") if err := fs.Parse(args); err != nil { @@ -362,25 +409,110 @@ func newFlagSet(name string, stderr io.Writer) *flag.FlagSet { return fs } -func takeFirstPositional(args []string) (string, []string, error) { - for i, arg := range args { +func hasHelp(args []string) bool { + for _, arg := range args { + if arg == "-h" || arg == "--help" || arg == "help" { + return true + } + } + return false +} + +func takeFirstPositional(args []string, valueFlags ...string) (string, []string, error) { + flagsWithValues := make(map[string]bool, len(valueFlags)) + for _, flagName := range valueFlags { + flagsWithValues[flagName] = true + } + for i := 0; i < len(args); i++ { + arg := args[i] + if arg == "--" { + if i+1 < len(args) { + return args[i+1], removeArgAt(args, i+1), nil + } + break + } if strings.HasPrefix(arg, "-") { + name := strings.TrimLeft(arg, "-") + if cut, _, ok := strings.Cut(name, "="); ok { + name = cut + } + if flagsWithValues[name] && !strings.Contains(arg, "=") && i+1 < len(args) { + i++ + } continue } - rest := make([]string, 0, len(args)-1) - rest = append(rest, args[:i]...) - rest = append(rest, args[i+1:]...) - return arg, rest, nil + return args[i], removeArgAt(args, i), nil } return "", nil, fmt.Errorf("missing positional argument") } +func removeArgAt(args []string, index int) []string { + rest := make([]string, 0, len(args)-1) + rest = append(rest, args[:index]...) + rest = append(rest, args[index+1:]...) + return rest +} + func writeUsage(w io.Writer) { _, _ = fmt.Fprintf(w, "Usage:\n") _, _ = fmt.Fprintf(w, " scion list [--json]\n") _, _ = fmt.Fprintf(w, " scion info [--json]\n") - _, _ = fmt.Fprintf(w, " scion add --to [--dry-run] [--force] [--standalone]\n") - _, _ = fmt.Fprintf(w, " scion diff --target [--json]\n") + _, _ = fmt.Fprintf(w, " scion add [--to ] [--dry-run] [--force] [--standalone]\n") + _, _ = fmt.Fprintf(w, " scion diff [--target ] [--json]\n") _, _ = fmt.Fprintf(w, " scion doctor [--strict] [--json]\n") _, _ = fmt.Fprintf(w, " scion version [--json]\n") + _, _ = fmt.Fprintf(w, "\nRun \"scion help \" for command details.\n") +} + +func writeCommandUsage(w io.Writer, command string) bool { + switch command { + case "list": + _, _ = fmt.Fprintf(w, "Usage: scion list [--json]\n\n") + _, _ = fmt.Fprintf(w, "List embedded source templates.\n\n") + _, _ = fmt.Fprintf(w, "Options:\n") + _, _ = fmt.Fprintf(w, " --json write JSON\n") + case "info": + _, _ = fmt.Fprintf(w, "Usage: scion info [--json]\n\n") + _, _ = fmt.Fprintf(w, "Show metadata for one embedded source template.\n\n") + _, _ = fmt.Fprintf(w, "Examples:\n") + _, _ = fmt.Fprintf(w, " scion info cache\n") + _, _ = fmt.Fprintf(w, " scion info auth --json\n\n") + _, _ = fmt.Fprintf(w, "Options:\n") + _, _ = fmt.Fprintf(w, " --json write JSON\n") + case "add": + _, _ = fmt.Fprintf(w, "Usage: scion add [--to ] [--dry-run] [--force] [--standalone]\n\n") + _, _ = fmt.Fprintf(w, "Copy a source template into your project. If --to is omitted, Scion uses the module default target.\n\n") + _, _ = fmt.Fprintf(w, "Examples:\n") + _, _ = fmt.Fprintf(w, " scion add cache --dry-run\n") + _, _ = fmt.Fprintf(w, " scion add cache\n") + _, _ = fmt.Fprintf(w, " scion add auth --standalone\n\n") + _, _ = fmt.Fprintf(w, "Options:\n") + _, _ = fmt.Fprintf(w, " --to destination directory; defaults to the module default target\n") + _, _ = fmt.Fprintf(w, " --dry-run show files without writing\n") + _, _ = fmt.Fprintf(w, " --force overwrite files copied by the bundle\n") + _, _ = fmt.Fprintf(w, " --standalone copy go.mod/go.sum for modules with external dependencies\n") + case "diff": + _, _ = fmt.Fprintf(w, "Usage: scion diff [--target ] [--json]\n\n") + _, _ = fmt.Fprintf(w, "Compare a copied module with the embedded source template. If --target is omitted, Scion uses the module default target.\n\n") + _, _ = fmt.Fprintf(w, "Examples:\n") + _, _ = fmt.Fprintf(w, " scion diff cache\n") + _, _ = fmt.Fprintf(w, " scion diff cache --target internal/cache\n\n") + _, _ = fmt.Fprintf(w, "Options:\n") + _, _ = fmt.Fprintf(w, " --target destination directory; defaults to the module default target\n") + _, _ = fmt.Fprintf(w, " --json write JSON\n") + case "doctor": + _, _ = fmt.Fprintf(w, "Usage: scion doctor [--strict] [--json]\n\n") + _, _ = fmt.Fprintf(w, "Check registry, bundle, and module release readiness.\n\n") + _, _ = fmt.Fprintf(w, "Options:\n") + _, _ = fmt.Fprintf(w, " --strict treat release-gate warnings as errors\n") + _, _ = fmt.Fprintf(w, " --json write JSON\n") + case "version": + _, _ = fmt.Fprintf(w, "Usage: scion version [--json]\n\n") + _, _ = fmt.Fprintf(w, "Print CLI version, commit, registry version, and bundle hash.\n\n") + _, _ = fmt.Fprintf(w, "Options:\n") + _, _ = fmt.Fprintf(w, " --json write JSON\n") + default: + return false + } + return true } diff --git a/internal/cli/cli_test.go b/internal/cli/cli_test.go index aac89d8..678d19c 100644 --- a/internal/cli/cli_test.go +++ b/internal/cli/cli_test.go @@ -23,6 +23,20 @@ func newTestApp(t *testing.T) *cli.App { return cli.New(reg) } +func chdir(t *testing.T, dir string) { + t.Helper() + old, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + if err := os.Chdir(dir); err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + _ = os.Chdir(old) + }) +} + func TestAddCacheAndDiffInTemporaryGoProject(t *testing.T) { app := newTestApp(t) project := t.TempDir() @@ -71,6 +85,37 @@ func TestAddCacheAndDiffInTemporaryGoProject(t *testing.T) { } } +func TestAddAndDiffUseDefaultTarget(t *testing.T) { + app := newTestApp(t) + project := t.TempDir() + chdir(t, project) + if err := os.WriteFile(filepath.Join(project, "go.mod"), []byte("module example.com/app\n\ngo 1.22\n"), 0o644); err != nil { + t.Fatal(err) + } + + var stdout, stderr bytes.Buffer + code := app.Run(context.Background(), []string{"add", "cache"}, &stdout, &stderr) + if code != 0 { + t.Fatalf("add exit %d, stdout=%q stderr=%q", code, stdout.String(), stderr.String()) + } + if !strings.Contains(stdout.String(), "internal/cache") { + t.Fatalf("add output should mention default target: %q", stdout.String()) + } + if _, err := os.Stat(filepath.Join(project, "internal", "cache", ".scion-module.json")); err != nil { + t.Fatalf("metadata missing at default target: %v", err) + } + + stdout.Reset() + stderr.Reset() + code = app.Run(context.Background(), []string{"diff", "cache"}, &stdout, &stderr) + if code != 0 { + t.Fatalf("diff exit %d, stdout=%q stderr=%q", code, stdout.String(), stderr.String()) + } + if !strings.Contains(stdout.String(), "cache matches embedded registry version") { + t.Fatalf("unexpected diff output: %q", stdout.String()) + } +} + func TestAddRefusesExternalDependencyModuleWithoutStandalone(t *testing.T) { app := newTestApp(t) var stdout, stderr bytes.Buffer @@ -83,6 +128,58 @@ func TestAddRefusesExternalDependencyModuleWithoutStandalone(t *testing.T) { } } +func TestInfoShowsStandaloneHintForExternalDependencyModule(t *testing.T) { + app := newTestApp(t) + var stdout, stderr bytes.Buffer + code := app.Run(context.Background(), []string{"info", "auth"}, &stdout, &stderr) + if code != 0 { + t.Fatalf("info exit %d, stderr=%q", code, stderr.String()) + } + out := stdout.String() + for _, want := range []string{ + "Standard library only: false", + "Standalone required: true", + "scion add auth --standalone", + } { + if !strings.Contains(out, want) { + t.Fatalf("info output missing %q: %q", want, out) + } + } +} + +func TestCommandHelp(t *testing.T) { + app := newTestApp(t) + tests := [][]string{ + {"help", "add"}, + {"add", "--help"}, + {"diff", "-h"}, + } + for _, args := range tests { + var stdout, stderr bytes.Buffer + code := app.Run(context.Background(), args, &stdout, &stderr) + if code != 0 { + t.Fatalf("%v exit %d, stdout=%q stderr=%q", args, code, stdout.String(), stderr.String()) + } + if !strings.Contains(stdout.String(), "Usage: scion") { + t.Fatalf("%v help missing usage: %q", args, stdout.String()) + } + } +} + +func TestFlagsCanPrecedeModule(t *testing.T) { + app := newTestApp(t) + project := t.TempDir() + target := filepath.Join(project, "internal", "cache") + var stdout, stderr bytes.Buffer + code := app.Run(context.Background(), []string{"add", "--to", target, "--dry-run", "cache"}, &stdout, &stderr) + if code != 0 { + t.Fatalf("add exit %d, stdout=%q stderr=%q", code, stdout.String(), stderr.String()) + } + if !strings.Contains(stdout.String(), "Would copy") { + t.Fatalf("unexpected add output: %q", stdout.String()) + } +} + func TestListJSON(t *testing.T) { app := newTestApp(t) var stdout, stderr bytes.Buffer