Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ builds:
- arm64
env:
- CGO_ENABLED=0
ldflags:
- -X github.com/shpoont/dotfiles-manager/internal/app.buildVersion={{ .Version }}
- id: darwin
main: ./cmd/dotfiles-manager
binary: dotfiles-manager
Expand All @@ -22,6 +24,8 @@ builds:
- arm64
env:
- CGO_ENABLED=0
ldflags:
- -X github.com/shpoont/dotfiles-manager/internal/app.buildVersion={{ .Version }}
archives:
- id: default
ids:
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Core workflows:
- `status` — preview drift and candidate operations
- `deploy` — apply source -> target
- `import` — apply target -> source (managed updates + optional unmanaged/missing rules)
- `version` / `--version` — print CLI version and exit

---

Expand Down Expand Up @@ -124,13 +125,16 @@ syncs:
2) Run commands (using default config discovery in current directory):

```bash
dotfiles-manager --version
dotfiles-manager status
dotfiles-manager deploy --dry-run ~/.config/nvim
dotfiles-manager deploy ~/.config/nvim
dotfiles-manager import --dry-run ~/.config/nvim
dotfiles-manager import ~/.config/nvim
```

`--version`/`version` prints `dotfiles-manager version <value>` and exits (`dev` on local non-release builds).

3) Optional explicit override:

```bash
Expand Down
8 changes: 8 additions & 0 deletions cmd/dotfiles-manager/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ func TestRunReturnsZeroForHelp(t *testing.T) {
require.Equal(t, 0, run())
}

func TestRunReturnsZeroForVersion(t *testing.T) {
oldArgs := os.Args
t.Cleanup(func() { os.Args = oldArgs })
os.Args = []string{"dotfiles-manager", "--version"}

require.Equal(t, 0, run())
}

func TestMainUsesExitHook(t *testing.T) {
oldArgs := os.Args
oldExit := osExit
Expand Down
6 changes: 5 additions & 1 deletion docs/internal/contracts/validation-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ This document defines stable error codes and when they are raised.

All runtime errors are fail-fast and return non-zero exit.
When `--json` is set, errors are emitted via JSON envelope (`ok=false`, `error.code`, `error.message`, optional `error.details`).
`version`/`--version` bypass config loading and normally do not traverse this validation pipeline.

## 1) Exit codes

Expand All @@ -23,7 +24,7 @@ When `--json` is set, errors are emitted via JSON envelope (`ok=false`, `error.c

| Code | Trigger | Message template |
|---|---|---|
| `DFM_CONFIG_REQUIRED` | no config source resolved from `--config`, `DOTFILES_MANAGER_CONFIG`, or `./.dotfiles-manager.yaml` in cwd | `Config not found: pass --config, set DOTFILES_MANAGER_CONFIG, or create ./.dotfiles-manager.yaml` |
| `DFM_CONFIG_REQUIRED` | no config source resolved for config-dependent commands (`status`/`deploy`/`import`) from `--config`, `DOTFILES_MANAGER_CONFIG`, or `./.dotfiles-manager.yaml` in cwd | `Config not found: pass --config, set DOTFILES_MANAGER_CONFIG, or create ./.dotfiles-manager.yaml` |
| `DFM_CONFIG_NOT_FOUND` | config path does not exist | `Config file not found: {config_path}` |
| `DFM_CONFIG_NOT_FILE` | config path exists but is not a regular file | `Config path is not a file: {config_path}` |
| `DFM_CONFIG_PARSE` | YAML parse failure | `Failed to parse YAML config: {config_path}` |
Expand Down Expand Up @@ -82,3 +83,6 @@ To keep errors deterministic:
5. runtime filesystem operations

Stop at first failure.

Exception:
- `version` and `--version` short-circuit before config/path/runtime validation.
3 changes: 3 additions & 0 deletions docs/internal/engineering/acceptance-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ Use this checklist before calling implementation complete.
- [ ] any validation/runtime error exits `1`.
- [ ] runtime failures are fail-fast.
- [ ] `status --dry-run` fails with `DFM_FLAG_UNSUPPORTED`.
- [ ] `dotfiles-manager version` prints `dotfiles-manager version <value>` and exits `0`.
- [ ] `dotfiles-manager --version` prints `dotfiles-manager version <value>` and exits `0`.
- [ ] `version`/`--version` work without config present.

## I) Logging and observability

Expand Down
1 change: 1 addition & 0 deletions docs/internal/engineering/ci-cd.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ Manual procedure:
2. Verify artifact checksum.
3. Run binary in isolated temp repo/temp HOME:
- `dotfiles-manager --help`
- `dotfiles-manager --version`
- `dotfiles-manager status`
- `dotfiles-manager deploy --dry-run`
- `dotfiles-manager import --dry-run`
Expand Down
2 changes: 2 additions & 0 deletions docs/internal/engineering/testing-strategy.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ This document defines the test structure that validates the specification.
- operation planning and ordering

2. **Integration tests**
- CLI behavior for version/status/deploy/import
- real filesystem scenarios for deploy/import/status
- overlapping sync behavior (config order; later sync wins)
- metadata behavior by contract
Expand Down Expand Up @@ -106,6 +107,7 @@ internal/testkit/
- redaction/masking paths: full branch coverage
- error logging branches (including `DFM_*` codes): full branch coverage
- per-command integration assertions (`status`/`deploy`/`import`):
- `version`/`--version` return expected format, do not require config, and do not perform sync filesystem operations
- logs are written to platform-default log file path
- `--log-file` overrides destination path
- default logging level is `info`
Expand Down
12 changes: 10 additions & 2 deletions docs/internal/scope/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ This document defines the runtime architecture that implements the approved spec
## 2) Runtime module boundaries

1. **CLI / command layer**
- parses global flags (`--config`, `--log-file`, `--log-level`) and command flags
- dispatches `status`, `deploy`, `import`
- parses global flags (`--version`, `--config`, `--log-file`, `--log-level`) and command flags
- dispatches `version`, `status`, `deploy`, `import`
- handles global `--version` short-circuit
- maps unsupported/invalid flags to stable error codes

2. **Config resolver + validator**
Expand Down Expand Up @@ -79,6 +80,13 @@ This document defines the runtime architecture that implements the approved spec
- no executor write phase
- status-only candidate sets are reported

### version

`CLI -> version reporter`

- no config resolver, scope resolver, planner, or executor path
- outputs `dotfiles-manager version <value>` and exits

### deploy

`CLI -> config resolver -> scope resolver -> planner -> executor -> reporter`
Expand Down
4 changes: 3 additions & 1 deletion docs/internal/scope/product-scope.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
owner: Product + Core Engineering
status: Implementation-ready
last-updated: 2026-02-16
last-updated: 2026-02-17
canonical-source: docs/internal/scope/product-scope.md
---

Expand All @@ -15,13 +15,15 @@ canonical-source: docs/internal/scope/product-scope.md

## Core commands (scope)

- `version` / `--version` — report CLI version
- `status` — preview drift and candidate operations
- `deploy` — apply source -> target
- `import` — apply target -> source within configured rules

## In-scope behavior

- Config-driven sync definitions (`syncs`)
- Version reporting command (`version` / `--version`)
- Path-scoped execution with optional `[path]`
- Pattern-driven unmanaged import and unmanaged removal behavior
- Missing-path import deletion behavior by include/exclude patterns
Expand Down
5 changes: 5 additions & 0 deletions docs/internal/specs/cli-and-config-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ Contract-level details are in `../contracts/*`.
## Command surface

```text
dotfiles-manager --version
dotfiles-manager version
dotfiles-manager [--config <path>] [--log-file <path>] [--log-level <debug|info|warn|error>] status [--json] [path]
dotfiles-manager [--config <path>] [--log-file <path>] [--log-level <debug|info|warn|error>] deploy [--dry-run] [--json] [path]
dotfiles-manager [--config <path>] [--log-file <path>] [--log-level <debug|info|warn|error>] import [--dry-run] [--json] [path]
Expand All @@ -24,6 +26,8 @@ Rules:
- config resolution order: `--config <path>` → `DOTFILES_MANAGER_CONFIG` → `./.dotfiles-manager.yaml` (cwd)
- default lookup is cwd-only (no parent search)
- default config filename is `.dotfiles-manager.yaml`
- `version` / `--version` bypass config resolution and print version immediately
- `version` does not accept `[path]`, `--json`, or `--dry-run`
- logs are written to platform-default log file path unless overridden with `--log-file`
- no log format flag is supported; logs are human-readable text only
- log level defaults to `info`; supported levels: `debug`, `info`, `warn`, `error`
Expand Down Expand Up @@ -66,6 +70,7 @@ Machine-readable schema:

## Behavior summary

- `version`/`--version`: print `dotfiles-manager version <value>` and exit (`dev` for non-release local builds)
- `status`: report drift and candidate sets
- `deploy`: source -> target; optional unmanaged removal by patterns
- `import`: target -> source; optional unmanaged adds + optional missing deletes by patterns
Expand Down
1 change: 1 addition & 0 deletions docs/internal/specs/decision-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Canonical rules and rationale live in **`decisions.md`**.

| Command | Direction | Base scope | Pattern sets used | Outcome focus |
|---|---|---|---|---|
| `version` / `--version` | info | n/a | none | Reports CLI version and exits. |
| `status [--json] [path]` | compare | Manifest + candidates | add-unmanaged include/exclude, remove-unmanaged, remove-missing include/exclude | Reports drift + candidate sets. |
| `deploy [--dry-run] [--json] [path]` | S → T | Manifest paths | remove-unmanaged | Applies copy/remove behavior (or plans only with `--dry-run`). |
| `import [--dry-run] [--json] [path]` | T → S | Manifest paths | add-unmanaged include/exclude, remove-missing include/exclude | Applies import behavior (or plans only with `--dry-run`). |
Expand Down
9 changes: 9 additions & 0 deletions docs/internal/specs/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ Pattern-based behavior is config-driven only.

## 4) Command semantics

### `version` and `--version`

- `dotfiles-manager version` and `dotfiles-manager --version` are equivalent.
- Output format is one line: `dotfiles-manager version <value>`.
- These paths bypass config resolution and sync planning/execution.
- Release builds print semantic version; local non-release builds print `dev`.
- `version` does not support `[path]`, `--json`, or `--dry-run`.

### `status [--json] [path]`

Reports:
Expand Down Expand Up @@ -113,6 +121,7 @@ Metadata guarantees and best-effort behavior are defined in `../contracts/metada
|---|---|
| Commands are fail-fast on runtime errors | Prevents partial hidden failures. |
| Exit `0` on success (including `status` with drift), non-zero on errors | Conventional CLI semantics. |
| `version` and `--version` print version and exit without loading config | Keeps version checks lightweight and robust. |
| `--json` supported on `status`, `deploy`, and `import` | Machine-readable automation support. |
| `--dry-run` supported on `deploy` and `import`, not `status` | Keeps preview explicit for mutating commands; `status` is already preview-only. |
| Text output suppresses empty phase blocks | Reduces noise and surfaces only actionable work. |
Expand Down
6 changes: 5 additions & 1 deletion docs/user/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ Main commands:
- `status` (preview)
- `deploy` (source → target)
- `import` (target → source)
- `version` / `--version` (print version and exit)

## Important baseline

- Config is required for every run, resolved in this order:
- Config is required for `status`/`deploy`/`import`, resolved in this order:
1. `--config <path>`
2. `DOTFILES_MANAGER_CONFIG`
3. `./.dotfiles-manager.yaml` in the current working directory
Expand All @@ -35,5 +36,8 @@ Main commands:
- logs are always human-readable text (no log format option)
- Log level defaults to `info`; use `--log-level` to change verbosity.
- Warnings/errors are emitted as human-readable diagnostics on stderr.
- `dotfiles-manager version` and `dotfiles-manager --version` print version and exit without loading config.
- release builds print semantic version
- local non-release builds print `dev`

For deeper implementation/spec details, see `../internal/README.md`.
20 changes: 20 additions & 0 deletions docs/user/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
## Command format

```text
dotfiles-manager --version
dotfiles-manager version
dotfiles-manager [--config <path>] [--log-file <path>] [--log-level <debug|info|warn|error>] status [--json] [path]
dotfiles-manager [--config <path>] [--log-file <path>] [--log-level <debug|info|warn|error>] deploy [--dry-run] [--json] [path]
dotfiles-manager [--config <path>] [--log-file <path>] [--log-level <debug|info|warn|error>] import [--dry-run] [--json] [path]
Expand All @@ -14,6 +16,7 @@ Config is resolved in this order:
3. `./.dotfiles-manager.yaml` in the current working directory

No parent-directory config search is performed.
`version`/`--version` do not require config resolution.

Log file destination:
- default paths:
Expand All @@ -32,6 +35,21 @@ stderr diagnostics:
- warnings and errors are emitted as human-readable text on stderr
- stdout remains command output (including `--json`)

## `version` and `--version`

Both commands print a single line and exit:

```text
dotfiles-manager version 0.1.4
```

Behavior:
- `dotfiles-manager version` and `dotfiles-manager --version` are equivalent
- they do not load config
- they do not accept `[path]`, `--json`, or `--dry-run`
- release builds print semantic version
- local non-release builds print `dev`

## `status [--json] [path]`

Reports:
Expand Down Expand Up @@ -113,6 +131,8 @@ If `[path]` matches no syncs, command fails.

## Output and exit codes

- `version`/`--version` output one line: `dotfiles-manager version <value>`.
- `version`/`--version` exit `0` and do not require config.
- text mode prints per-sync sections with exact file operations.
- every sync header uses:
- `sync[idx] target=~/<target> source=./<source>`
Expand Down
14 changes: 14 additions & 0 deletions docs/user/faq.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,3 +67,17 @@ Yes. Use `--log-level <debug|info|warn|error>`.

- default log level is `info`
- applies to all commands

## How do I check CLI version?

Use either:

```bash
dotfiles-manager --version
# or
dotfiles-manager version
```

Both print `dotfiles-manager version <value>` and exit.
They do not require config.
Release builds print semantic version; local non-release builds print `dev`.
11 changes: 11 additions & 0 deletions docs/user/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@ Or install with Go:
go install github.com/shpoont/dotfiles-manager/cmd/dotfiles-manager@latest
```

## 1.1) Check installed version

```bash
dotfiles-manager --version
# or
dotfiles-manager version
```

Release builds show semantic version.
Local non-release builds show `dev`.

## 2) Prepare your repo

Create or choose a repo where your managed dotfiles live.
Expand Down
16 changes: 16 additions & 0 deletions internal/app/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,18 +32,34 @@ func NewRootCmd() *cobra.Command {
SilenceErrors: true,
SilenceUsage: true,
}
rootCmd.Version = currentVersion()
rootCmd.SetVersionTemplate("dotfiles-manager version {{.Version}}\n")
rootCmd.Flags().Bool("version", false, "Print version and exit")

rootCmd.PersistentFlags().StringVar(&opts.configPath, "config", "", "Path to config file")
rootCmd.PersistentFlags().StringVar(&opts.logFile, "log-file", "", "Path to log file")
rootCmd.PersistentFlags().StringVar(&opts.logLevel, "log-level", "info", "Log level: debug|info|warn|error")

rootCmd.AddCommand(newVersionCmd())
rootCmd.AddCommand(newStatusCmd(opts))
rootCmd.AddCommand(newDeployCmd(opts))
rootCmd.AddCommand(newImportCmd(opts))

return rootCmd
}

func newVersionCmd() *cobra.Command {
return &cobra.Command{
Use: "version",
Short: "Print version information",
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
_, _ = fmt.Fprintln(cmd.OutOrStdout(), versionLine())
return nil
},
}
}

func newStatusCmd(opts *rootOptions) *cobra.Command {
var jsonOutput bool
var dryRun bool
Expand Down
20 changes: 20 additions & 0 deletions internal/app/version.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package app

import (
"fmt"
"strings"
)

var buildVersion = "dev"

func currentVersion() string {
version := strings.TrimSpace(buildVersion)
if version == "" {
return "dev"
}
return version
}

func versionLine() string {
return fmt.Sprintf("dotfiles-manager version %s", currentVersion())
}
Loading