Repository navigation
feat(config): read ~/.testsprite/config for persistent defaults (fixes #100) #540
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,7 +10,7 @@ import { | |
| resolveRequestTimeoutMs, | ||
| type CommonOptions as FactoryCommonOptions, | ||
| } from '../lib/client-factory.js'; | ||
| import { resolveProfileName } from '../lib/config.js'; | ||
| import { loadConfig, resolveProfileName } from '../lib/config.js'; | ||
| import { ApiError, InterruptError, RequestTimeoutError } from '../lib/errors.js'; | ||
| import { firstSuiteNextSteps } from '../lib/first-suite-hint.js'; | ||
| import type { FetchImpl, HttpClient } from '../lib/http.js'; | ||
|
|
@@ -2131,6 +2131,10 @@ function requireDocsProjectId(projectId: string | undefined, deps: ProjectDeps): | |
| if (explicit && explicit.length > 0) return explicit; | ||
| const envValue = (deps.env ?? process.env).TESTSPRITE_PROJECT_ID?.trim(); | ||
| if (envValue && envValue.length > 0) return envValue; | ||
| // Lowest-precedence persistent default from ~/.testsprite/config, composing | ||
| // with TESTSPRITE_PROJECT_ID (flag > env > config). | ||
| const configProjectId = loadConfig({ env: deps.env ?? process.env }).projectId; | ||
| if (configProjectId && configProjectId.length > 0) return configProjectId; | ||
|
Comment on lines
+2136
to
+2137
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '2115,2155p' src/commands/project.ts
rg -n 'requireDocsProjectId|credentialsPath|profile:' src/commands/project.ts | tail -65Repository: TestSprite/testsprite-cli Length of output: 2195 🏁 Script executed: set -e
printf '%s\n' '--- loadConfig definitions/usages ---'
rg -n -F --glob '*.ts' -e 'function loadConfig' -e 'const loadConfig' -e 'loadConfig(' src test tests 2>/dev/null || true
printf '%s\n' '--- config/profile symbols ---'
rg -n -F --glob '*.ts' -e 'readProfile' -e 'assertValidProfileName' -e 'resolveProfileName' -e 'interface ProjectDeps' -e 'type ProjectDeps' src test tests 2>/dev/null || true
printf '%s\n' '--- project command context ---'
sed -n '90,155p' src/commands/project.ts
sed -n '1035,1110p' src/commands/project.ts
sed -n '1860,1920p' src/commands/project.ts
printf '%s\n' '--- config implementation candidates ---'
rg --files src test tests 2>/dev/null | grep -E 'config|credential|profile' | head -80Repository: TestSprite/testsprite-cli Length of output: 21349 🏁 Script executed: set -e
printf '%s\n' '--- config implementation ---'
sed -n '1,125p' src/lib/config.ts
sed -n '1,75p' src/lib/credentials.ts
printf '%s\n' '--- docs upload registration and callers ---'
rg -n -F --glob '*.ts' -e 'runDocsUpload' -e 'docs upload' -e 'createProjectCommand' src/index.ts src/commands/project.ts src/commands/*.ts
sed -n '300,440p' src/index.ts
printf '%s\n' '--- project command factory and docs registration ---'
rg -n -F --glob '*.ts' -e 'function createProjectCommand' -e 'docsCommand' -e 'upload' src/commands/project.ts
sed -n '2500,2750p' src/commands/project.ts
printf '%s\n' '--- error handling ---'
rg -n -F --glob '*.ts' -e 'localValidationError' -e 'catch (err' -e 'catch (error' src/index.ts src/commands/project.ts src/lib/errors.tsRepository: TestSprite/testsprite-cli Length of output: 27601 🏁 Script executed: set -e
printf '%s\n' '--- upload command action ---'
sed -n '1438,1785p' src/commands/project.ts
printf '%s\n' '--- local validation helper and error mapping ---'
sed -n '2160,2190p' src/commands/project.ts
sed -n '380,475p' src/lib/errors.ts
sed -n '430,525p' src/index.ts
printf '%s\n' '--- docs upload tests for project/config/profile/path ---'
sed -n '150,180p' src/commands/project.docs.spec.ts
sed -n '580,675p' src/commands/project.docs.spec.ts
sed -n '930,990p' src/commands/project.docs.spec.tsRepository: TestSprite/testsprite-cli Length of output: 29797 Pass the resolved profile and credentials path to project-ID lookup.
When no project flag or environment value exists, an invalid nonblank Suggested fix-function requireDocsProjectId(projectId: string | undefined, deps: ProjectDeps): string {
+function requireDocsProjectId(
+ projectId: string | undefined,
+ deps: ProjectDeps,
+ profile?: string,
+): string {
const explicit = projectId?.trim();
if (explicit && explicit.length > 0) return explicit;
const envValue = (deps.env ?? process.env).TESTSPRITE_PROJECT_ID?.trim();
if (envValue && envValue.length > 0) return envValue;
- const configProjectId = loadConfig({ env: deps.env ?? process.env }).projectId;
+ const configProjectId = loadConfig({
+ profile,
+ env: deps.env ?? process.env,
+ credentialsPath: deps.credentialsPath,
+ }).projectId;
if (configProjectId && configProjectId.length > 0) return configProjectId;- const projectId = requireDocsProjectId(opts.projectId, deps);
+ const projectId = requireDocsProjectId(opts.projectId, deps, opts.profile);🤖 Prompt for AI Agents |
||
| throw localValidationError( | ||
| '--project <id> is required; pass --project <id> or set TESTSPRITE_PROJECT_ID', | ||
| ); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -16168,7 +16168,10 @@ function resolveProjectId(projectId: string | undefined, deps: TestDeps): string | |
| if (explicit && explicit.length > 0) return explicit; | ||
| const envValue = (deps.env ?? process.env).TESTSPRITE_PROJECT_ID; | ||
| const trimmed = envValue?.trim(); | ||
| return trimmed && trimmed.length > 0 ? trimmed : undefined; | ||
| if (trimmed && trimmed.length > 0) return trimmed; | ||
| // Lowest-precedence persistent default from ~/.testsprite/config for the | ||
| // resolved profile, composing with TESTSPRITE_PROJECT_ID (flag > env > config). | ||
| return loadConfig({ env: deps.env ?? process.env }).projectId; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win The
🤖 Prompt for AI Agents |
||
| } | ||
| function requireProjectId( | ||
| projectId: string | undefined, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,7 +19,7 @@ import { createTunnelCommand } from './commands/tunnel.js'; | |
| import { createTestCommand } from './commands/test.js'; | ||
| import { createTestListCommand } from './commands/testlist.js'; | ||
| import { createUsageCommand } from './commands/usage.js'; | ||
| import { resolveProfileName } from './lib/config.js'; | ||
| import { loadConfig, resolveProfileName } from './lib/config.js'; | ||
| import { classifyBillingRefusal } from './lib/billing-refusal.js'; | ||
| import { TARGETS, type AgentTarget } from './lib/agent-targets.js'; | ||
| import { | ||
|
|
@@ -415,7 +415,13 @@ try { | |
| process.exitCode = code; | ||
| }; | ||
|
|
||
| const rawMode = program.opts<{ output?: string }>().output; | ||
| // Persistent default --output from ~/.testsprite/config sits below the flag | ||
| // (flag > env > config-file > built-in 'text'); the flag wins when present. | ||
| const config = loadConfig({ | ||
| profile: resolveProfileName(program.opts<{ profile?: string }>().profile), | ||
| env: process.env, | ||
| }); | ||
| const rawMode = program.opts<{ output?: string }>().output ?? config.output; | ||
|
Comment on lines
+420
to
+424
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '400,452p' src/index.ts
sed -n '75,110p' src/lib/config.ts
sed -n '185,220p' src/lib/credentials.tsRepository: TestSprite/testsprite-cli Length of output: 5009 🏁 Script executed: set -eu
printf '%s\n' '--- src/index.ts error-handler context ---'
nl -ba src/index.ts | sed -n '350,485p'
printf '%s\n' '--- profile resolution and output validation definitions ---'
rg -n -F --glob '*.ts' -e 'function resolveProfileName' -e 'export function resolveProfileName' -e 'function isOutputMode' -e 'export function isOutputMode' -e 'class Output' -e 'export class Output' src
printf '%s\n' '--- referenced definitions ---'
rg -n -F --glob '*.ts' -e 'resolveProfileName' -e 'isOutputMode' -e 'class Output' srcRepository: TestSprite/testsprite-cli Length of output: 11149 Guard
Suggested fix--- "a/src/index.ts"
+++ "b/src/index.ts"
@@ -417,11 +417,16 @@
// Persistent default --output from ~/.testsprite/config sits below the flag
// (flag > env > config-file > built-in 'text'); the flag wins when present.
- const config = loadConfig({
- profile: resolveProfileName(program.opts<{ profile?: string }>().profile),
- env: process.env,
- });
- const rawMode = program.opts<{ output?: string }>().output ?? config.output;
+ let configOutput: string | undefined;
+ try {
+ configOutput = loadConfig({
+ profile: resolveProfileName(program.opts<{ profile?: string }>().profile),
+ env: process.env,
+ }).output;
+ } catch {
+ configOutput = undefined;
+ }
+ const rawMode = program.opts<{ output?: string }>().output ?? configOutput;
const mode = isOutputMode(rawMode) ? rawMode : 'text';
const output = new Output(mode);
if (err instanceof ApiError) {
🤖 Prompt for AI Agents |
||
| const mode = isOutputMode(rawMode) ? rawMode : 'text'; | ||
| const output = new Output(mode); | ||
| if (err instanceof ApiError) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,13 +6,16 @@ export interface Config { | |
| apiUrl: string; | ||
| apiKey?: string; | ||
| profile: string; | ||
| output?: string; | ||
| projectId?: string; | ||
| } | ||
|
|
||
| export interface LoadConfigOptions { | ||
| profile?: string; | ||
| endpointUrl?: string; | ||
| env?: NodeJS.ProcessEnv; | ||
| credentialsPath?: string; | ||
| configPath?: string; | ||
| } | ||
|
|
||
| export const DEFAULT_API_URL = 'https://api.testsprite.com'; | ||
|
|
@@ -50,21 +53,33 @@ export function resolveProfileName( | |
| } | ||
|
|
||
| /** | ||
| * Resolves the active profile name and its (apiUrl, apiKey) pair. | ||
| * Resolves the active profile name and its (apiUrl, apiKey) pair, plus the | ||
| * persistent settings carried by the config file (`~/.testsprite/config`). | ||
| * | ||
| * Resolution order, highest precedence first: | ||
| * profile name: options.profile > env.TESTSPRITE_PROFILE > "default" | ||
| * apiKey: env.TESTSPRITE_API_KEY > credentials file profile entry | ||
| * apiUrl: options.endpointUrl > env.TESTSPRITE_API_URL > credentials file > built-in default | ||
| * apiUrl: options.endpointUrl > env.TESTSPRITE_API_URL > | ||
| * credentials file > config file > built-in default | ||
| * output: --output flag (applied by the caller) > config file | ||
| * projectId: --project flag > env.TESTSPRITE_PROJECT_ID > config file | ||
| * | ||
| * Env wins over the credentials file so CI / scripted callers can run without touching | ||
| * the user's ~/.testsprite/credentials. | ||
| * The config file is the lowest-precedence layer above the built-in defaults, | ||
| * so `flag > env > config-file > built-in default` holds for every setting it | ||
| * can hold (`output`, `endpoint_url`/`api_url`, `project`). API credentials | ||
| * (`api_key`) stay in `~/.testsprite/credentials` only — this mirrors the | ||
| * aws-cli split (credentials in one file, settings in another). | ||
| * | ||
| * Env wins over the credentials file so CI / scripted callers can run without | ||
| * touching the user's ~/.testsprite/credentials. | ||
| */ | ||
| export function loadConfig(options: LoadConfigOptions = {}): Config { | ||
| const env = options.env ?? process.env; | ||
| const profile = resolveProfileName(options.profile, env); | ||
| const credentialsPath = options.credentialsPath ?? defaultCredentialsPath(); | ||
| const fileEntry = readProfile(profile, { path: credentialsPath }); | ||
| const configPath = options.configPath ?? env.TESTSPRITE_CONFIG_FILE ?? defaultConfigPath(); | ||
| const credentialsEntry = readProfile(profile, { path: credentialsPath }); | ||
| const configEntry = readProfile(profile, { path: configPath }); | ||
|
|
||
| // Empty / whitespace-only env vars are treated as unset so they do not | ||
| // short-circuit the `??` chain (e.g. `export TESTSPRITE_API_URL=` or | ||
|
|
@@ -77,8 +92,15 @@ export function loadConfig(options: LoadConfigOptions = {}): Config { | |
| const envApiKey = normalizeEnvVar(env.TESTSPRITE_API_KEY); | ||
|
|
||
| return { | ||
| apiUrl: options.endpointUrl ?? envApiUrl ?? fileEntry?.apiUrl ?? DEFAULT_API_URL, | ||
| apiKey: envApiKey ?? fileEntry?.apiKey, | ||
| apiUrl: | ||
| options.endpointUrl ?? | ||
| envApiUrl ?? | ||
| credentialsEntry?.apiUrl ?? | ||
| configEntry?.apiUrl ?? | ||
| DEFAULT_API_URL, | ||
| apiKey: envApiKey ?? credentialsEntry?.apiKey, | ||
| profile, | ||
| output: configEntry?.output, | ||
| projectId: configEntry?.projectId, | ||
|
Comment on lines
+103
to
+104
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: sed -n '66,83p;408,429p' src/index.ts
sed -n '1880,1905p' src/commands/project.ts
sed -n '95,108p' src/lib/config.ts
rg -n 'resolveOutputMode\(globals.output\)|resolveCommonOptions\(' src/commands | head -65Repository: TestSprite/testsprite-cli Length of output: 8954 Apply When The error handler has the same precedence problem: Resolve the configured fallback before successful command option resolution, while retaining the explicit command-line value when present. Use the same resolved value in the error handler. 🤖 Prompt for AI Agents |
||
| }; | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
The precedence description omits the credentials file and
TESTSPRITE_CONFIG_FILE.For
api_url, the credentials file ranks above the config file.TESTSPRITE_CONFIG_FILEis not documented in the environment variable table. Add both.🤖 Prompt for AI Agents
Source: Path instructions