Skip to content

Add global and local profile scopes#35

Open
bgelatti wants to merge 12 commits into
masterfrom
feat/cli-global-config
Open

Add global and local profile scopes#35
bgelatti wants to merge 12 commits into
masterfrom
feat/cli-global-config

Conversation

@bgelatti

@bgelatti bgelatti commented May 7, 2026

Copy link
Copy Markdown
Contributor

Closes tago-io/project-sdk-and-tools#4.

Problem

The CLI resolves credentials strictly from cwd, forcing users to either share one profile across every project or manually swap configs when switching. Running from a subdirectory of a project errors out, and there is no per-user, project-independent profile.

Investigation

  • Every credential read in the codebase already funnels through getEnvironmentConfig(), readToken(), and the dotenv path computation. Centralizing scope resolution in those three helpers means the 27+ command call sites do not need to change.
  • The XDG Base Directory Spec gives us a portable global location (~/.config/tagoio on Unix, %APPDATA%/tagoio on Windows) without adding a dependency.
  • Analysis development inherently requires a project directory (the analysis source files live there), so analysis-* commands stay local-only. Devices, dashboards, profiles, and exports work in either scope.

Solution

A new resolveScope() primitive walks from cwd up the parent chain (capped at 32 levels, logical-only — matches git/npm convention) looking for tagoconfig.json. First match wins as local; if no ancestor matches, falls back to the platform-specific global directory. The decision is invisible to command-layer code.

What's new

  • tagoio whoami — offline, scope-aware command. Prints scope, loaded path, active env, profile id/name/email, and a hard-coded loaded/missing token indicator (token bytes are never read into the output payload).
  • --scope local|global flag on init and login. No flag falls through to the decision tree (local-exists → edit local; global-exists → edit global; neither → prompt; default fallback creates local).
  • Stderr scope banner on every mutating command ([INFO] Using <scope> profile (<path>)). Suppressed by --silent so CI flows stay clean.
  • One-time stderr notice on first run after upgrade, suppressed by a per-user sentinel at ~/.tagoio/.scope-notice-shown so it cannot land in version control.

Security guarantees

  • Global config dir created with mode 0o700; lock files written with 0o600 (unreadable by other local users).
  • Symlinked global dir is refused with an actionable error.
  • whoami snapshot test asserts the token UUID never appears in stdout/stderr.

Backward compatibility

Existing users see no behavior change except the one-time notice. Their on-disk layout (<project>/tagoconfig.json + .tago-lock.<env>.lock + .tagoio/personal.env) is unchanged — the resolver finds it via parent-walk. Running from a subdirectory of a project now works (today's CLI errors out).

Branch context

This is branched from feat/cli-man-page, so the PR diff is scoped to the global/local profile changes only. Prior man-page and refactoring work lives in their own PRs.

bgelatti added 7 commits May 5, 2026 13:52
Pull the command-registration logic out of initiateCMD into a standalone
buildProgram(defaultEnv) that returns a configured Command without
parsing argv. The runtime CLI keeps the same behaviour while the new
function becomes the single source of truth for the man-page generator.
Guard the auto-run with an import.meta.url check so importing this
module from a build tool no longer triggers program.parse().
Add a build-time roff generator at src/lib/generate-man.ts that walks
buildProgram() and emits a single tagoio.1 page. Wire npm run man into
the build chain plus prepublishOnly, ship the file via package.json
files and man fields, gitignore the artifact, and document the install
path (man tagoio plus fish_update_completions for fish users) in the
README. A vitest snapshot test acts as the drift gate so CI fails when
a flag or command changes without a matching snapshot regeneration; a
companion mandoc/groff integration test catches roff syntax errors.
The interactive prompt-driven flows (start-config, backup restore,
export-setup) are excluded from the coverage threshold since they are
covered by manual smokes, not unit tests.
The FILES section claimed ~/.tago-lock but the runtime writes per-environment
locks at ./.tago-lock.<env>.lock (verified in src/lib/token.ts), and was
missing ./.tagoio/personal.env where set-env persists TAGOIO_DEFAULT. Add an
EXIT STATUS section documenting 0 (success) and 1 (any failure via
errorHandler) as clig.dev recommends. Snapshot regenerated to match.
CI's `npm ci` was failing because package.json declared
oxfmt@^0.47.0 / oxlint@1.62.0 but the committed lockfile still
resolved to ^0.46.0 / 1.61.0. Regenerated the lockfile so
transitive @oxfmt/* and @oxlint/* bindings align with the
declared versions.
Replaces the developer-oriented stdout/stderr/JSON breakdown with a
purpose-first summary: official CLI to TagoIO, manages the four
top-level resources, suitable for interactive and CI/CD use.
Closes tago-io/project-sdk-and-tools#4. Adds a parent-walk resolver that finds
tagoconfig.json from any subdirectory and falls back to a per-user global
config under XDG/AppData. New `tagoio whoami` command and `--scope local|global`
flag on init/login, with mutating-command stderr banner, secure 0o700/0o600
perms on global, and analysis-* commands gated to local scope.
Six test files (login, start-config, four analysis-* commands) called
resolveScope/requireLocalScope without mocking, which fails on hosts where
the project root has no tagoconfig.json. Added resolve-scope and
scope-notice mocks to each, plus a defensive setScopeOverride(undefined)
reset in resolve-scope.test.ts so module-level state can't leak.
@bgelatti bgelatti changed the base branch from feat/cli-man-page to master May 7, 2026 13:08
bgelatti added 2 commits May 7, 2026 10:40
Static text bodies (description, exit-status, environment, files, see-also,
author) move out of the inline roff arrays in generate-man.ts and into a
dedicated MAN_CONTENT module written as plain English. The generator wraps
each value with escapeRoff() and emits the same structural roff. Editing prose
no longer requires counting backslashes.
Comment thread src/commands/analysis/deploy.ts
@bgelatti bgelatti requested a review from vitorfdl May 7, 2026 14:59
bgelatti and others added 3 commits May 8, 2026 08:56
- print the scope banner on data --post and dashboard copy-tab, two
  mutating commands that previously wrote remote state without showing
  which profile they were about to touch
- extract a pure readConfigFile() helper (read + parse, no auto-create,
  no status output) and have whoami use it instead of duplicating the
  read/parse, keeping whoami strictly read-only
- add the scope-notice / resolve-scope mocks to the data-post and copy-tab
  tests, matching the sibling pattern, so the banner does not leak to stderr
@mateuscardosodeveloper

Copy link
Copy Markdown
Collaborator

Reviewed the global/local profile work and ran the full functional test pass against a real TagoIO profile, then applied the findings and revalidated everything live.

Improvements applied in this branch:

  1. Scope banner on every mutating command. Two mutating commands wrote remote state without printing which profile they were about to touch: device data --post (postDeviceData) and dashboard copy-tab (copyTabWidgets). Both now call printScopeBanner as their first action, matching the sibling commands. Verified live: tagoio data --post prints the banner before inserting (and does not double print, since getDeviceData delegates and returns), and tagoio copy-tab prints it before any dashboard mutation.

  2. whoami no longer duplicates the config read. Extracted a pure readConfigFile() helper in config-file.ts (read plus parse, never auto creates the file, no status output) and pointed whoami at it instead of its own readFileSync plus parse. This removes the duplication and keeps whoami strictly read only, without depending on a distant guard to stay non mutating.

Functional test coverage (live, against the [Mateus] Second Profile):
whoami in table and JSON form, parent walk from a deep subdirectory (the key new behavior, resolves the project config from a nested cwd), global fallback outside any project, whoami error path with no global config (actionable message), requireLocalScope blocking analysis run and analysis deploy outside a project, scope banner on a mutating command, init --scope global creating and authenticating a global profile, whoami reading the global profile, local and global coexistence (parent walk prefers local), and the S1 security guarantee (global dir 0700, lock files and personal.env 0600) confirmed on disk.

Full suite green (486 tests), tsc clean, 0 new lint warnings.

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.

2 participants