internal/commands/README.md
The commands package hosts the CLI implementation for the PhotoPrism binary. Command wiring begins in commands.go, where each *cli.Command is registered on the shared slice consumed by cmd/photoprism/photoprism.go. Supporting utilities such as flag builders, shared error handling, and helper structs are colocated with their related command files. Keep commands cohesive: each file should focus on a single functional area (for example, download.go for the downloader entry point and download_impl.go for reusable logic). Whenever you introduce new commands, align naming with existing patterns and expose --json or --yes options when automation benefits from them.
*cli.Command to the PhotoPrism slice in commands.go.Before hook when a command needs configuration loading or authentication checks that differ from the defaults.internal/config for option binding instead of reimplementing flag parsing. Field definitions belong in the shared flag modules so photoprism show config-options stays accurate.<name>_impl.go files that can be imported in tests. Invoke the implementation from the Action function to avoid duplicating logic between CLI entry points and tests.flags.go (YesFlag(), DryRunFlag(), role helpers, etc.) so identical options behave consistently across commands. If you need a new reusable flag, add it to flags.go first and then consume it from each command instead of hand-coding variants.filepath.Join and rely on permission constants from pkg/fs (fs.ModeDir, fs.ModeFile, and friends) when writing to disk.force flags) before replacing non-empty files. Where replacements are expected, open destinations with O_WRONLY|O_CREATE|O_TRUNC.event.Log rather than direct fmt printing. Sensitive information such as secrets or tokens must never be logged.*config.Config (for example, conf.ClusterUUID()) rather than mutating option structs directly.pkg/http/safe or the specialized wrappers in internal/thumb/avatar to inherit timeout, size, and SSRF protection defaults.internal/config/options.go with the appropriate struct tags (yaml, json, flag) so they propagate to YAML, CLI, and API layers consistently.internal/config/flags.go to keep environment variable mappings aligned. Commands should call conf.ApplyCliContext() once to hydrate configuration from parsed flags.options.yml. Commands that generate configuration must set c.Options().OptionsYaml before persisting so changes appear in reports.internal/commands/catalog instead of crafting ad-hoc Markdown or JSON.<name>_test.go) and group related assertions using t.Run("CaseName", ...) subtests. Subtest names should use PascalCase for readability.go test ./internal/commands -run '<Name>' -count=1 during development. For broader coverage, make test-go exercises backend packages under SQLite.RunWithTestContext(cmd, args) so urfave/cli exit codes do not call os.Exit during tests. If you only need to inspect the exit status, invoke cmd.Action(ctx) directly and assert cli.ExitCoder.config.NewTestConfig("commands") when migrations and fixtures are required, config.NewMinimalTestConfig(t.TempDir()) when the test needs only filesystem scaffolding, or config.NewMinimalTestConfigWithDb("commands", t.TempDir()) for an isolated SQLite schema without heavy fixtures.conf.InitializeTestData() when constructing custom configs so Originals, Import, Cache, and Temp paths exist before tests interact with the filesystem.rnd.GenerateUID(entity.PhotoUID) or rnd.UUIDv7() instead of hard-coded strings.go test ./internal/commands -run 'DownloadImpl|DownloadHelp' -count=1go test ./internal/commands -run 'Auth|Users' -count=1go test ./internal/commands -run 'Cluster' -count=1go test ./internal/commands -count=1go test ./internal/service/cluster/registry -count=1 and go test ./internal/api -run 'Cluster' -count=1 ensure CLI and API stay in sync before release.yt-dlp with lightweight shell scripts that honor --dump-single-json and --print requests. Support environment variables like YTDLP_ARGS_LOG, YTDLP_OUTPUT_FILE, and YTDLP_DUMMY_CONTENT to capture arguments, create deterministic artifacts, and avoid duplicate detection in importer flows.conf.Options().FFmpegBin = "/bin/false" and conf.Settings().Index.Convert = false.pkg/http/header (for example, header.ContentTypeZip) to keep expectations aligned with middleware.internal/auth/acl such as acl.ParseRole, acl.ScopePermits, and acl.ScopeAttrPermits instead of duplicating logic inside commands.make fmt-go and make swag-fmt swaggo build ./...go test ./internal/commands -run '<Name>' -count=1go test ./internal/commands -run 'ClusterRegister|ClusterNodesRotate' -count=1 and go test ./internal/api -run 'Cluster' -count=1photoprism show commands --json --nested (or the Markdown default) should reflect the new entries without manual editing.