Skip to content

Add tog config subcommand (set, get, path, init)#79

Merged
toddhainsworth merged 9 commits into
mainfrom
feature/61_tog-config-command-surface
May 18, 2026
Merged

Add tog config subcommand (set, get, path, init)#79
toddhainsworth merged 9 commits into
mainfrom
feature/61_tog-config-command-surface

Conversation

@toddhainsworth

Copy link
Copy Markdown
Owner

Implements all vertical slices for #61.

Slices

Test plan

  • go test ./... — all packages pass
  • gofmt -l . — clean
  • Smoke test on a real Toggl account: tog config init end-to-end (token entry, workspace selection, file written with 0600)
  • Smoke test the env-var-aware path: TOGGL_API_TOKEN=… tog config init — workspace-only flow, on-disk token field empty
  • Verify tog config get resolved view matches actual runtime behaviour on a system with both global config and .tog.yml

Capture the architectural decisions behind the upcoming `tog config`
verbs (set/get/path/init), the key × scope matrix, asymmetric token
input model, resolved-view pure function, secrets-manager
non-resolution stance, module boundaries, and permissions/atomicity.

Fixes #70
Establish the `tog config` Cobra group and ship the first read-only
verb: `path`. Bare prints the global config path (honours
XDG_CONFIG_HOME). `--local` walks up for `.tog.yml`; on a miss, prints
"would be created at <path>" using the git toplevel or PWD fallback.

Exports `config.Path()` and adds `projectcfg.ResolveWritePath(cwd)`
which subsequent slices reuse for the `--local` write path.

Fixes #71
Add `internal/configview.Resolve` as a pure function that produces a
value + source view for token/workspace/project given a global config,
project config, and environment lookup. This is the single source of
truth for precedence and is used by `tog config get`.

The `get` command supports:
- Bare form: prints all three keys with resolved value + source.
- Single-key form: just that key's value + source (script-friendly).
- `--global` / `--local`: filters to one file rather than the resolved view.
- Token redacted to `****<last-4>` by default; `--reveal` prints the
  full token for the rare migration case.

Fixes #72
internal/config gains Save / SaveTo: atomic temp-file-then-rename in
the target directory, parent dir created with 0700 if missing, and
file perms clamped to 0600 on every write — including over an existing
0644 file.

`tog config set workspace <id>` writes through to the global config,
mapping the CLI key `workspace` to YAML `default_workspace_id`. The
key × scope matrix rejects `set project --global` (no project field
in global), unknown keys, and non-numeric workspace values with
specific error messages. `set token <value>` is rejected with the
security message; bare `set token` returns a not-yet-implemented
placeholder pending #75.

Fixes #73
internal/projectcfg gains Save: atomic temp-file-then-rename in the
target directory and 0644 clamp on every write.

`tog config set --local` writes through to the project .tog.yml
discovered via ResolveWritePath, preserving any existing fields. The
new path is printed to stderr on first creation so the user knows
where future invocations from a subdirectory will find the file.

`tog config set token --local` is rejected with the security-specific
message: the token must not be committed to .tog.yml.

Fixes #74
Replace the placeholder rejection with the real token-input path. Bare
`set token` opens a huh password prompt (echo mode hidden); `--stdin`
reads a single line from stdin for piping from secrets managers
(`op read | tog config set token --stdin`).

The positional form is still rejected — a bare value would leak the
token to shell history, scrollback, and session recordings. The error
points at the two safe alternatives.

Fixes #75
internal/toggl gains a Workspace struct, ListWorkspaces() against
GET /me/workspaces, and an ErrUnauthorized sentinel wrapping 401
responses so init can loop the token prompt on rejection without
string-matching the API error message.

tog config init drives the bootstrap flow: TTY-only, prompts for
overwrite when the global config already has values, asks for the
token (huh password input), validates via /me, loops on 401 and
offers retry on network errors. With exactly one workspace the
choice is auto-selected; otherwise a huh selector picks. The
resulting config is written atomically with 0600 perms via the
existing config.SaveTo writer.

Fixes #76
Detect TOGGL_API_TOKEN at the start of init and offer to keep the
token in the vault rather than write it to disk — the workflow for
op run / 1Password users who want only the workspace choice to
persist. Declining the save-to-disk prompt validates against the
env-var token via /me, runs the workspace flow, and writes a config
with an empty token field.

Accepting falls through to the normal token-prompt path from #76.

Fixes #77
Three new/updated documents now describe the shipped tog config
surface:

- docs/commands/config.md — full reference for init/set/get/path,
  the key × scope matrix, and worked examples for new-user init,
  token rotation, per-project defaults, and the get debugging flow.
- docs/secrets.md — op run patterns for both native and
  containerised use, plus brief Bitwarden and pass notes so the page
  isn't 1Password-only. Documents the wrapper-script env-var
  passthrough and the bind-mounted set --stdin behaviour.
- README — config row added to the Command Reference table; the
  Configuration section points at docs/secrets.md.

Fixes #78
@toddhainsworth toddhainsworth merged commit 8c680c8 into main May 18, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant