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