1+ import { existsSync , readFileSync } from 'node:fs' ;
12import { homedir } from 'node:os' ;
23import { join } from 'node:path' ;
3- import { DEFAULT_PROFILE , defaultCredentialsPath , readProfile } from './credentials.js' ;
4+ import {
5+ DEFAULT_PROFILE ,
6+ defaultCredentialsPath ,
7+ parseIniFile ,
8+ readProfile ,
9+ } from './credentials.js' ;
410
511export interface Config {
612 apiUrl : string ;
@@ -13,6 +19,8 @@ export interface LoadConfigOptions {
1319 endpointUrl ?: string ;
1420 env ?: NodeJS . ProcessEnv ;
1521 credentialsPath ?: string ;
22+ /** Settings file override; defaults to `TESTSPRITE_CONFIG_FILE` or `~/.testsprite/config`. */
23+ configPath ?: string ;
1624}
1725
1826const DEFAULT_API_URL = 'https://api.testsprite.com' ;
@@ -21,25 +29,87 @@ export function defaultConfigPath(): string {
2129 return join ( homedir ( ) , '.testsprite' , 'config' ) ;
2230}
2331
32+ /**
33+ * Non-credential defaults a user can persist in `~/.testsprite/config`
34+ * (aws-cli's credentials-vs-config split: the key lives in `credentials`,
35+ * settings live here). INI sections are profile names, mirroring the
36+ * credentials file.
37+ */
38+ export interface ConfigFileSettings {
39+ endpointUrl ?: string ;
40+ output ?: string ;
41+ projectId ?: string ;
42+ }
43+
44+ /** INI key -> settings field. sourceRef: DOCUMENTATION.md configuration table. */
45+ const CONFIG_KEY_TO_FIELD : Record < string , keyof ConfigFileSettings > = {
46+ endpoint_url : 'endpointUrl' ,
47+ output : 'output' ,
48+ project_id : 'projectId' ,
49+ } ;
50+
51+ export interface ReadConfigFileOptions {
52+ env ?: NodeJS . ProcessEnv ;
53+ path ?: string ;
54+ }
55+
56+ /**
57+ * Read the `[profile]` section of the settings config file. Missing or
58+ * unreadable file (or section) yields `{}` so the cascade falls through to
59+ * built-in defaults; the file is optional by design. Reuses the hardened INI
60+ * parser the credentials file uses (null-prototype + proto-key guards).
61+ */
62+ export function readConfigFileSettings (
63+ profile : string ,
64+ options : ReadConfigFileOptions = { } ,
65+ ) : ConfigFileSettings {
66+ const env = options . env ?? process . env ;
67+ const path = options . path ?? env . TESTSPRITE_CONFIG_FILE ?? defaultConfigPath ( ) ;
68+ let content : string ;
69+ try {
70+ if ( ! existsSync ( path ) ) return { } ;
71+ content = readFileSync ( path , 'utf-8' ) ;
72+ } catch {
73+ // Unreadable settings file: settings are optional, never fatal.
74+ return { } ;
75+ }
76+ const section = parseIniFile ( content ) [ profile ] ;
77+ if ( ! section ) return { } ;
78+ const settings : ConfigFileSettings = { } ;
79+ for ( const [ rawKey , field ] of Object . entries ( CONFIG_KEY_TO_FIELD ) ) {
80+ const value = section [ rawKey ] ;
81+ if ( typeof value === 'string' && value . length > 0 ) settings [ field ] = value ;
82+ }
83+ return settings ;
84+ }
85+
2486/**
2587 * Resolves the active profile name and its (apiUrl, apiKey) pair.
2688 *
2789 * Resolution order, highest precedence first:
2890 * profile name: options.profile > env.TESTSPRITE_PROFILE > "default"
2991 * apiKey: env.TESTSPRITE_API_KEY > credentials file profile entry
30- * apiUrl: options.endpointUrl > env.TESTSPRITE_API_URL > credentials file > built-in default
92+ * apiUrl: options.endpointUrl > env.TESTSPRITE_API_URL > credentials file
93+ * > config file `endpoint_url` > built-in default
3194 *
3295 * Env wins over the credentials file so CI / scripted callers can run without touching
33- * the user's ~/.testsprite/credentials.
96+ * the user's ~/.testsprite/credentials. The config file sits just above the built-in
97+ * default: it is where a user persists "this machine talks to this endpoint" once.
3498 */
3599export function loadConfig ( options : LoadConfigOptions = { } ) : Config {
36100 const env = options . env ?? process . env ;
37101 const profile = options . profile ?? env . TESTSPRITE_PROFILE ?? DEFAULT_PROFILE ;
38102 const credentialsPath = options . credentialsPath ?? defaultCredentialsPath ( ) ;
39103 const fileEntry = readProfile ( profile , { path : credentialsPath } ) ;
104+ const settings = readConfigFileSettings ( profile , { env, path : options . configPath } ) ;
40105
41106 return {
42- apiUrl : options . endpointUrl ?? env . TESTSPRITE_API_URL ?? fileEntry ?. apiUrl ?? DEFAULT_API_URL ,
107+ apiUrl :
108+ options . endpointUrl ??
109+ env . TESTSPRITE_API_URL ??
110+ fileEntry ?. apiUrl ??
111+ settings . endpointUrl ??
112+ DEFAULT_API_URL ,
43113 apiKey : env . TESTSPRITE_API_KEY ?? fileEntry ?. apiKey ,
44114 profile,
45115 } ;
0 commit comments