diff --git a/.env.example b/.env.example index bed417b..5bf91d6 100644 --- a/.env.example +++ b/.env.example @@ -2,3 +2,5 @@ WHOOP_CLIENT_ID= WHOOP_CLIENT_SECRET= WHOOP_REDIRECT_URI= +# Optional: custom path for OAuth token storage (default: ~/.whoop-cli/tokens.json) +WHOOP_TOKEN_FILE= diff --git a/README.md b/README.md index c0a3a1e..f800ca5 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,8 @@ Before using, you need to configure WHOOP API credentials: export WHOOP_CLIENT_ID=your_client_id export WHOOP_CLIENT_SECRET=your_client_secret export WHOOP_REDIRECT_URI=https://your-redirect-uri.com/callback +# Optional: override token storage path for multi-account setups +export WHOOP_TOKEN_FILE=~/.whoop-cli/tokens-personal.json ``` Or create a `.env` file in your working directory. @@ -45,7 +47,9 @@ Or create a `.env` file in your working directory. whoopskill auth login ``` -Tokens are stored in `~/.whoop-cli/tokens.json` and auto-refresh when expired. +Tokens are stored in `~/.whoop-cli/tokens.json` by default and auto-refresh when expired. + +Set `WHOOP_TOKEN_FILE` to override token location (useful for multiple users/accounts on one machine). ## Usage @@ -96,7 +100,7 @@ Important: - For automation, you must call `whoopskill auth refresh` periodically. Recommended pattern: -- Run `whoopskill auth login` once interactively (creates `~/.whoop-cli/tokens.json`). +- Run `whoopskill auth login` once interactively (creates the default `~/.whoop-cli/tokens.json`, or your `WHOOP_TOKEN_FILE` path if set). - Run a small periodic monitor that calls `whoopskill auth refresh` and performs a lightweight fetch. An example monitor script + systemd timer/cron examples are included here: diff --git a/src/auth/tokens.ts b/src/auth/tokens.ts index 6033681..ede141b 100644 --- a/src/auth/tokens.ts +++ b/src/auth/tokens.ts @@ -1,23 +1,38 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync } from 'node:fs'; import { homedir } from 'node:os'; -import { join } from 'node:path'; +import { dirname, join, resolve } from 'node:path'; import type { TokenData, OAuthTokenResponse } from '../types/whoop.js'; import { WhoopError, ExitCode } from '../utils/errors.js'; -const CONFIG_DIR = join(homedir(), '.whoop-cli'); -const TOKEN_FILE = join(CONFIG_DIR, 'tokens.json'); +const DEFAULT_TOKEN_FILE = join(homedir(), '.whoop-cli', 'tokens.json'); + +function getTokenFilePath(): string { + const envPath = process.env.WHOOP_TOKEN_FILE?.trim(); + if (!envPath) { + return DEFAULT_TOKEN_FILE; + } + + // Resolve relative paths against current working directory + return resolve(envPath); +} + +function getConfigDir(): string { + return dirname(getTokenFilePath()); +} // Refresh tokens 15 minutes before expiry to avoid race conditions const REFRESH_BUFFER_SECONDS = 900; function ensureConfigDir(): void { - if (!existsSync(CONFIG_DIR)) { - mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 }); + const configDir = getConfigDir(); + if (!existsSync(configDir)) { + mkdirSync(configDir, { recursive: true, mode: 0o700 }); } } export function saveTokens(response: OAuthTokenResponse): void { ensureConfigDir(); + const tokenFile = getTokenFilePath(); const data: TokenData = { access_token: response.access_token, @@ -27,17 +42,18 @@ export function saveTokens(response: OAuthTokenResponse): void { scope: response.scope, }; - writeFileSync(TOKEN_FILE, JSON.stringify(data, null, 2)); - chmodSync(TOKEN_FILE, 0o600); + writeFileSync(tokenFile, JSON.stringify(data, null, 2)); + chmodSync(tokenFile, 0o600); } export function loadTokens(): TokenData | null { - if (!existsSync(TOKEN_FILE)) { + const tokenFile = getTokenFilePath(); + if (!existsSync(tokenFile)) { return null; } try { - const content = readFileSync(TOKEN_FILE, 'utf-8'); + const content = readFileSync(tokenFile, 'utf-8'); return JSON.parse(content) as TokenData; } catch { return null; @@ -45,8 +61,9 @@ export function loadTokens(): TokenData | null { } export function clearTokens(): void { - if (existsSync(TOKEN_FILE)) { - writeFileSync(TOKEN_FILE, ''); + const tokenFile = getTokenFilePath(); + if (existsSync(tokenFile)) { + writeFileSync(tokenFile, ''); } }