Skip to content

Multi-environment support #34

Description

@tarik02

Summary

Add multiple named auth environments to one t3cli install, so one CLI config can hold credentials for local, staging, and remote t3code servers.

Current master state checked: package t3code-cli@0.11.0 at 1d46c60.

Current state

  • Auth commands live in src/cli/auth.ts and currently expose only auth pair, auth local, and auth status.
  • Config service lives in src/config/service.ts + src/config/layer.ts.
  • Stored config is flat: { url?, token?, local? } at ~/.config/t3cli/config.json or $XDG_CONFIG_HOME/t3cli/config.json.
  • T3Config.resolve() already applies T3CODE_URL / T3CODE_TOKEN above stored config.
  • Runtime connections are centralized through T3Config.resolve() in src/runtime/layer.ts, so environment selection belongs there.
  • Public package exports include ./config and ./auth; exported types change with the config shape.

Proposed decisions

Environment names

Environment names are non-empty strings containing only [A-Za-z0-9._-].

Selection precedence

  1. Resolve the base stored environment from --environment, then T3CLI_ENV, then config default.
  2. If both T3CODE_URL and T3CODE_TOKEN are set, they override the selected environment connection.
  3. If exactly one of T3CODE_URL or T3CODE_TOKEN is set, fail with a config error instead of mixing env and stored credentials.
  4. If both env vars participate, resolved config source is env; otherwise config.

If selected environment does not exist, fail before trying to connect.

Project/thread scope

Per environment. Projects and thread IDs belong to the selected server.

Default names

  • auth local default name: local
  • auth pair --url <url> default name: hostname slug from paired server URL
  • --name <name> overrides default name for both commands

Pair/local write behavior

  • successful auth pair / auth local writes the named environment
  • written environment becomes default only when it is the first stored environment
  • otherwise default changes only via auth use <name>
  • duplicate name follows replacement rules below

Duplicate names

If --name already exists:

Context Behavior
--replace overwrite that environment and make it default
interactive human TTY prompt Environment '<name>' already exists. Replace? [y/N], default no
non-interactive, no --replace fail with message to pass --replace

Use existing Effect CLI prompt style from src/cli/confirm.ts.

Credential storage

Config file mode is 0600. Do not use group-readable or group-writable modes like 0640 / 0660.

V2 config stores environment metadata in plaintext and stores tokens only as encrypted blobs. No raw token is written to v2 config.

Encryption:

  • cipher: AES-256-GCM
  • key: random local master key
  • AAD: schema version + environment name + url + local flag

Key source:

  1. OS keyring master key by default.
  2. Automatic fallback to local key file at ~/.config/t3cli/key or $XDG_CONFIG_HOME/t3cli/key with mode 0600 when keyring is unavailable.

Do not add user-facing backend/status reporting for which key source was used.

Resolved questions

  1. Resolved: auth pair / auth local makes the written environment default only when creating the first environment. After that, default changes only via auth use <name>.
  2. Resolved: T3CODE_URL and T3CODE_TOKEN must be set together. Setting only one fails instead of mixing env and stored credentials.
  3. Resolved: auth unpair requires confirmation. Human TTY prompts; non-interactive mode requires --yes.
  4. Resolved: when deleting the default environment, clear default even if other environments remain. User must run auth use <name> or pass --environment.
  5. Resolved: environment names are strict slugs: non-empty, only [A-Za-z0-9._-].
  6. Resolved: auth list --format json includes stored fields only: name, url, local, default, and active. It does not contact servers or store extra token metadata.

Config schema v2

{
  "version": 2,
  "default": "home",
  "environments": {
    "home": {
      "url": "https://example.com",
      "local": false,
      "token": {
        "kind": "encrypted",
        "alg": "aes-256-gcm",
        "key": "default",
        "nonce": "...",
        "ciphertext": "...",
        "tag": "..."
      }
    },
    "local": {
      "url": "http://127.0.0.1:3000",
      "local": true,
      "token": {
        "kind": "encrypted",
        "alg": "aes-256-gcm",
        "key": "default",
        "nonce": "...",
        "ciphertext": "...",
        "tag": "..."
      }
    }
  }
}

Current v1 flat config is read as v2 with one environment:

  • env name: local when local: true, otherwise hostname slug from url, otherwise default
  • default: migrated env name
  • empty/missing config remains unauthenticated

All auth/config mutations write v2. Do not write flat v1.

Possible last-env removal shape:

{ "version": 2, "environments": {} }

CLI surface

t3cli --environment <name> ...      # global runtime selection
t3cli auth pair --url <url> [--name <name>] [--replace] [--local] [--format json]
t3cli auth local [--name <name>] [--replace] [existing local flags] [--format json]
t3cli auth list [--format json]
t3cli auth use <name> [--format json]
t3cli auth unpair [--name <name>] [--yes] [--format json]
t3cli auth status [--format json]

auth status reports active environment name when config-backed, plus current url, local, source, role, and expiry.

auth list never prints tokens. JSON includes only stored fields: name, url, local, default, and active.

auth use <name> only changes default; it does not contact the server.

auth unpair

Scope for this issue: local credential removal only.

Server self-revoke is not supported by t3code for the current session (current_session_revoke_not_allowed), so no revoke flow belongs in this issue. Document that auth unpair removes local CLI credentials and any remote token can remain valid until natural expiry.

Behavior:

t3cli auth unpair              # remove default environment
t3cli auth unpair --name work  # remove specific environment
t3cli auth unpair --yes        # skip confirmation; required in non-interactive mode

No token values are printed.

Use --yes rather than --force, matching existing destructive-confirm CLI convention.

Implementation checklist

  • Change config types/schema in src/config/service.ts and src/config/layer.ts to parse v1 flat + v2 named encrypted-token config.
  • Add environment selection to T3Config.resolve().
  • Add root --environment <name> and T3CLI_ENV support.
  • Extend auth write path in src/auth/layer.ts / src/auth/type.ts for named writes, encrypted token writes, and replacement behavior.
  • Add --name + --replace to auth pair and auth local.
  • Add auth list, auth use, and auth unpair.
  • Add credential encryption service: AES-256-GCM, OS keyring master key, automatic 0600 key-file fallback.
  • Update README + skills/t3code-cli/reference/setup.md + skills/t3code-cli/reference/commands.md.
  • Update exported config/auth types intentionally.

Acceptance criteria

  • Pair two servers under different names, switch with auth use, then run an existing command against the selected server.
  • --environment <name> overrides config default for one invocation.
  • T3CLI_ENV=<name> works when --environment is omitted.
  • T3CODE_URL / T3CODE_TOKEN override selected config only when both are set; setting exactly one fails.
  • Duplicate name aborts by default and overwrites only with --replace or confirmed prompt.
  • auth list hides tokens.
  • V2 config never stores raw tokens and keeps config/key files at 0600.
  • auth unpair removes local credentials, clears default when deleting the default environment, and documents token expiry behavior.

Out of scope

  • Dynamic shell completion implementation from CLI completions #38.
  • Server-side token revoke.
  • Project/thread migration between servers.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions