From 73a0bf3419801a5c583d7f7261da01c1a521dbe1 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 04:41:04 +0000 Subject: [PATCH 1/2] Say what to store, not where it goes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Storing a value meant knowing the key it lives under, so callers passed keys around: config.delete('current_workspace_id'), config.set(`${endpoint}.pat`, token), and a storeEndpoint helper for the three writes that go together. The config now answers in the CLI's own terms — setWorkspace, unsetWorkspace, getToken, setToken, unsetToken, setEndpoint — and the keys live in values.ts, used by that one file. Reading and writing per endpoint, dropping the legacy key, and clearing what belonged to the previous endpoint are its business rather than each caller's. seamPaths moves here too, as rootPaths: where the CLI keeps its files is config's own business, and the blueprint cache reads it from here rather than deriving the same root again. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QCJ1v1NFc6b43GooAhij2c --- package-lock.json | 48 ----------- src/bin/cli.ts | 6 +- src/lib/auth/operations.ts | 48 ++++------- src/lib/blueprint/source-npm.ts | 5 +- src/lib/config/config-store.ts | 29 ++++--- src/lib/config/index.ts | 13 ++- src/lib/config/memory-config-store.ts | 9 ++ src/lib/config/seam-config.ts | 91 ++++++++++++++++++++ src/lib/config/values.ts | 15 +++- src/lib/context.ts | 29 ++----- src/lib/interactions/endpoint-selection.ts | 4 +- src/lib/interactions/login.ts | 4 +- src/lib/interactions/workspace-id.ts | 4 +- test/auth/operations.test.ts | 97 ++++++++++++---------- test/context.test.ts | 42 +++++----- 15 files changed, 247 insertions(+), 197 deletions(-) create mode 100644 src/lib/config/seam-config.ts diff --git a/package-lock.json b/package-lock.json index 99fc9fd9..d53d276d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -100,9 +100,6 @@ "cpu": [ "arm64" ], - "libc": [ - "glibc" - ], "license": "SEE LICENSE IN LICENSE.md", "optional": true, "os": [ @@ -116,9 +113,6 @@ "cpu": [ "arm64" ], - "libc": [ - "musl" - ], "license": "SEE LICENSE IN LICENSE.md", "optional": true, "os": [ @@ -1275,9 +1269,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1295,9 +1286,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -1315,9 +1303,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -1335,9 +1320,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2116,9 +2098,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2133,9 +2112,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2150,9 +2126,6 @@ "loong64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2167,9 +2140,6 @@ "loong64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2184,9 +2154,6 @@ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2201,9 +2168,6 @@ "riscv64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -2218,9 +2182,6 @@ "riscv64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -2235,9 +2196,6 @@ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -6940,9 +6898,6 @@ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -6964,9 +6919,6 @@ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ diff --git a/src/bin/cli.ts b/src/bin/cli.ts index 4a156515..0cea5a45 100644 --- a/src/bin/cli.ts +++ b/src/bin/cli.ts @@ -19,7 +19,7 @@ import { buildRegistry, findLocalCommand, } from 'lib/commands/registry.js' -import { getConfigStore } from 'lib/config/index.js' +import { getConfig } from 'lib/config/index.js' import { type CliContext, resolveAuth } from 'lib/context.js' import { tokenEnvVar } from 'lib/env.js' import { reportErrorAndExit } from 'lib/errors.js' @@ -38,7 +38,7 @@ import { renderHelp } from 'lib/render/help.js' import seamapiCliVersion from 'lib/version.js' async function cli(args: ParsedArgs, argv: string[]) { - const config = getConfigStore() + const config = getConfig() const output = getOutput() const update = args['update'] === true @@ -136,7 +136,7 @@ async function cli(args: ParsedArgs, argv: string[]) { } const useRemoteApiDefs = - args['remote_api_defs'] ?? config.get('use_remote_api_defs') + args['remote_api_defs'] ?? config.getUseRemoteApiDefs() const blueprint = await getApiBlueprint({ useRemoteDefinitions: useRemoteApiDefs ?? false, diff --git a/src/lib/auth/operations.ts b/src/lib/auth/operations.ts index 795dd6ba..607f38bf 100644 --- a/src/lib/auth/operations.ts +++ b/src/lib/auth/operations.ts @@ -1,4 +1,4 @@ -import { type ConfigStore, getConfigStore } from 'lib/config/index.js' +import { getConfig, type SeamConfig } from 'lib/config/index.js' import { type AuthContext, resolveAuth } from 'lib/context.js' import { assertEnvVarUnset, @@ -60,7 +60,7 @@ export interface LoginOptions { */ export const login = async ( { endpoint, token, workspaceId }: LoginOptions, - config: ConfigStore = getConfigStore(), + config: SeamConfig = getConfig(), validate: typeof validateToken = validateToken, ): Promise => { let auth = resolveAuth(config) @@ -74,40 +74,37 @@ export const login = async ( } if (endpoint != null) { - storeEndpoint(endpoint, config) + config.setEndpoint(endpoint) auth = resolveAuth(config) } if (token != null) { await validate(token, workspaceId) - config.set(`${auth.endpoint}.pat`, token) - config.delete('current_workspace_id') + config.setToken(auth.endpoint, token) + config.unsetWorkspace() } if (workspaceId != null) { - config.set('current_workspace_id', workspaceId) + config.setWorkspace(workspaceId) } } /** Store the token for the current endpoint, e.g., one just prompted for. */ export const storeToken = ( token: string, - config: ConfigStore = getConfigStore(), + config: SeamConfig = getConfig(), ): void => { const auth = resolveAuth(config) assertMutable(auth, 'token', 'log in') - config.set(`${auth.endpoint}.pat`, token) + config.setToken(auth.endpoint, token) } /** Remove the stored token and workspace selection. */ -export const logout = (config: ConfigStore = getConfigStore()): void => { +export const logout = (config: SeamConfig = getConfig()): void => { const auth = resolveAuth(config) assertMutable(auth, 'token', 'log out') - config.delete(`${auth.endpoint}.pat`) - // Configs written before tokens were stored per endpoint may still hold an - // un-namespaced token, so drop that too. - config.delete('pat') - config.delete('current_workspace_id') + config.unsetToken(auth.endpoint) + config.unsetWorkspace() } /** @@ -117,36 +114,25 @@ export const logout = (config: ConfigStore = getConfigStore()): void => { */ export const selectEndpoint = ( endpoint: string, - config: ConfigStore = getConfigStore(), + config: SeamConfig = getConfig(), ): void => { assertMutable(resolveAuth(config), 'endpoint', 'select an endpoint') - storeEndpoint(endpoint, config) + config.setEndpoint(endpoint) } /** Store the workspace requests are made against. */ export const selectWorkspace = ( workspaceId: string, - config: ConfigStore = getConfigStore(), + config: SeamConfig = getConfig(), ): void => { assertMutable(resolveAuth(config), 'workspaceId', 'select a workspace') - config.set('current_workspace_id', workspaceId) + config.setWorkspace(workspaceId) } /** Store whether API definitions come from the endpoint instead of npm. */ export const setUseRemoteApiDefs = ( useRemoteApiDefs: boolean, - config: ConfigStore = getConfigStore(), + config: SeamConfig = getConfig(), ): void => { - config.set('use_remote_api_defs', useRemoteApiDefs) -} - -/** - * Write the endpoint, dropping what belonged to the previous one: the - * workspace selection, and any value left under the legacy `server` key that - * {@link resolveAuth} would otherwise still fall back to. - */ -const storeEndpoint = (endpoint: string, config: ConfigStore): void => { - config.set('endpoint', endpoint) - config.delete('server') - config.delete('current_workspace_id') + config.setUseRemoteApiDefs(useRemoteApiDefs) } diff --git a/src/lib/blueprint/source-npm.ts b/src/lib/blueprint/source-npm.ts index 9ca623d3..f3096fef 100644 --- a/src/lib/blueprint/source-npm.ts +++ b/src/lib/blueprint/source-npm.ts @@ -3,9 +3,9 @@ import { join } from 'node:path' import { pathToFileURL } from 'node:url' import type { Blueprint, TypesModuleInput } from '@seamapi/blueprint' -import envPaths from 'env-paths' import { extract } from 'tar' +import { rootPaths } from 'lib/config/index.js' import { withLoading } from 'lib/output/with-loading.js' import { @@ -38,8 +38,7 @@ export const getBlueprint = async ( options: GetBlueprintOptions = {}, ): Promise => { const update = options.update ?? false - const cacheDirectory = - options.cacheDirectory ?? envPaths('seam', { suffix: '' }).cache + const cacheDirectory = options.cacheDirectory ?? rootPaths.cache const cacheFile = getCacheFile(cacheDirectory) const blueprintVersion = await getBlueprintVersion() diff --git a/src/lib/config/config-store.ts b/src/lib/config/config-store.ts index 07c6fd7a..64ee8d4a 100644 --- a/src/lib/config/config-store.ts +++ b/src/lib/config/config-store.ts @@ -4,11 +4,14 @@ import Configstore from 'configstore' import envPaths from 'env-paths' import { migrateConfigStore } from './migrate.js' +import { createSeamConfig, type SeamConfig } from './seam-config.js' import { isStateKey, mergeConfig, splitConfig } from './values.js' const configFileName = 'cli.json' const legacyConfigStoreId = 'seam-cli' -const paths = envPaths('seam', { suffix: '' }) + +/** Every directory Seam keeps files in, for the CLI and for what it mounts. */ +export const rootPaths = envPaths('seam', { suffix: '' }) /** * What a config store can do, regardless of where it keeps the values. @@ -28,21 +31,21 @@ export interface ConfigStore { clear: () => void } -let configStore: ConfigStore | null = null +let config: SeamConfig | null = null -export const getConfigStore = (): ConfigStore => { - configStore ??= createConfigStore() - return configStore +export const getConfig = (): SeamConfig => { + config ??= createSeamConfig(createConfigStore()) + return config } -/** Replace the store, e.g., with an in-memory one for a test. */ -export const setConfigStore = (store: ConfigStore): void => { - configStore = store +/** Replace the config, e.g., with an in-memory one for a test. */ +export const setConfig = (nextConfig: SeamConfig): void => { + config = nextConfig } -/** Drop the current store so the next read builds the real one. */ -export const resetConfigStore = (): void => { - configStore = null +/** Drop the current config so the next read builds the real one. */ +export const resetConfig = (): void => { + config = null } const createConfigStore = (): PersistentConfigStore => { @@ -121,11 +124,11 @@ export class PersistentConfigStore implements ConfigStore { } const getConfigPath = (): string => { - return join(paths.config, configFileName) + return join(rootPaths.config, configFileName) } const getStateConfigPath = (): string => { - return join(paths.log, configFileName) + return join(rootPaths.log, configFileName) } const isRecord = (value: unknown): value is Record => { diff --git a/src/lib/config/index.ts b/src/lib/config/index.ts index 380110c5..afd703b3 100644 --- a/src/lib/config/index.ts +++ b/src/lib/config/index.ts @@ -1,8 +1,13 @@ export { type ConfigStore, - getConfigStore, + getConfig, type PersistentConfigStore, - resetConfigStore, - setConfigStore, + resetConfig, + rootPaths, + setConfig, } from './config-store.js' -export { createMemoryConfigStore } from './memory-config-store.js' +export { + createMemoryConfig, + createMemoryConfigStore, +} from './memory-config-store.js' +export { createSeamConfig, type SeamConfig } from './seam-config.js' diff --git a/src/lib/config/memory-config-store.ts b/src/lib/config/memory-config-store.ts index 372d08df..0dbfe923 100644 --- a/src/lib/config/memory-config-store.ts +++ b/src/lib/config/memory-config-store.ts @@ -1,4 +1,5 @@ import type { ConfigStore } from './config-store.js' +import { createSeamConfig, type SeamConfig } from './seam-config.js' /** * A real {@link ConfigStore} held in memory, for tests. @@ -60,3 +61,11 @@ export class MemoryConfigStore implements ConfigStore { export const createMemoryConfigStore = ( initialValues: Record = {}, ): ConfigStore => new MemoryConfigStore(initialValues) + +/** + * A config held in memory, for tests. Values may be seeded by key, e.g., to + * stand for a config an older CLI wrote. + */ +export const createMemoryConfig = ( + initialValues: Record = {}, +): SeamConfig => createSeamConfig(createMemoryConfigStore(initialValues)) diff --git a/src/lib/config/seam-config.ts b/src/lib/config/seam-config.ts new file mode 100644 index 00000000..3d30d0e3 --- /dev/null +++ b/src/lib/config/seam-config.ts @@ -0,0 +1,91 @@ +import type { ConfigStore } from './config-store.js' +import { + currentWorkspaceIdKey, + endpointKey, + getTokenKey, + legacyEndpointKey, + patKey, + useRemoteApiDefsKey, +} from './values.js' + +/** + * The CLI's configuration, in the terms the CLI thinks in. + * + * Every key lives in `values.ts` and is used here alone: a caller says what + * it wants stored, not where it goes or what it is called. + */ +export interface SeamConfig { + readonly path: string + getEndpoint: () => string | null + setEndpoint: (endpoint: string) => void + getToken: (endpoint: string) => string | null + setToken: (endpoint: string, token: string) => void + unsetToken: (endpoint: string) => void + getWorkspace: () => string | null + setWorkspace: (workspaceId: string) => void + unsetWorkspace: () => void + getUseRemoteApiDefs: () => boolean | null + setUseRemoteApiDefs: (useRemoteApiDefs: boolean) => void +} + +export const createSeamConfig = (store: ConfigStore): SeamConfig => ({ + get path() { + return store.path + }, + + getEndpoint: () => + readString(store.get(endpointKey)) ?? + readString(store.get(legacyEndpointKey)), + + /** + * Store the endpoint, dropping what belonged to the previous one: the + * workspace selection, and any value left under the legacy key that + * {@link SeamConfig.getEndpoint} would otherwise still fall back to. + */ + setEndpoint: (endpoint) => { + store.set(endpointKey, endpoint) + store.delete(legacyEndpointKey) + store.delete(currentWorkspaceIdKey) + }, + + /** Tokens are stored per endpoint, so one is never sent to another. */ + getToken: (endpoint) => readString(store.get(getTokenKey(endpoint))), + + setToken: (endpoint, token) => { + store.set(getTokenKey(endpoint), token) + }, + + unsetToken: (endpoint) => { + store.delete(getTokenKey(endpoint)) + // Configs written before tokens were stored per endpoint may still hold + // an un-namespaced one. + store.delete(patKey) + }, + + getWorkspace: () => readString(store.get(currentWorkspaceIdKey)), + + setWorkspace: (workspaceId) => { + store.set(currentWorkspaceIdKey, workspaceId) + }, + + unsetWorkspace: () => { + store.delete(currentWorkspaceIdKey) + }, + + getUseRemoteApiDefs: () => { + const useRemoteApiDefs = store.get(useRemoteApiDefsKey) + return typeof useRemoteApiDefs === 'boolean' ? useRemoteApiDefs : null + }, + + setUseRemoteApiDefs: (useRemoteApiDefs) => { + store.set(useRemoteApiDefsKey, useRemoteApiDefs) + }, +}) + +const readString = (value: unknown): string | null => { + if (typeof value !== 'string') return null + + const trimmedValue = value.trim() + + return trimmedValue === '' ? null : trimmedValue +} diff --git a/src/lib/config/values.ts b/src/lib/config/values.ts index 89931261..d7b611e4 100644 --- a/src/lib/config/values.ts +++ b/src/lib/config/values.ts @@ -4,8 +4,19 @@ * Pure transforms shared by the persistent store and the legacy migration. */ -const currentWorkspaceIdKey = 'current_workspace_id' -const patKey = 'pat' +export const endpointKey = 'endpoint' + +/** What the endpoint was called before, still read by `getEndpoint`. */ +export const legacyEndpointKey = 'server' + +export const currentWorkspaceIdKey = 'current_workspace_id' + +export const patKey = 'pat' + +export const useRemoteApiDefsKey = 'use_remote_api_defs' + +/** Tokens are stored per endpoint, e.g. `https://connect.getseam.com.pat`. */ +export const getTokenKey = (endpoint: string): string => `${endpoint}.${patKey}` /** Whether a key holds auth state rather than a setting. */ export const isStateKey = (key: string): boolean => { diff --git a/src/lib/context.ts b/src/lib/context.ts index 94e25dae..cf2147d4 100644 --- a/src/lib/context.ts +++ b/src/lib/context.ts @@ -1,6 +1,6 @@ import type { Interactivity } from './args/parse.js' import type { ApiBlueprint } from './blueprint/index.js' -import { type ConfigStore, getConfigStore } from './config/index.js' +import { getConfig, type SeamConfig } from './config/index.js' import { getEndpointFromEnv, getTokenFromEnv, @@ -30,23 +30,16 @@ export interface AuthContext { workspaceIdSource: Exclude | null } -export const resolveAuth = ( - config: ConfigStore = getConfigStore(), -): AuthContext => { +export const resolveAuth = (config: SeamConfig = getConfig()): AuthContext => { const envEndpoint = getEndpointFromEnv() - // Configs written before the endpoint was called one still hold it under - // `server`, so fall back to that key rather than silently resetting them. - const storedEndpoint = config.get('endpoint') ?? config.get('server') - const endpoint = - envEndpoint ?? (typeof storedEndpoint === 'string' ? storedEndpoint : null) + const storedEndpoint = config.getEndpoint() + const endpoint = envEndpoint ?? storedEndpoint const envToken = getTokenFromEnv() - const storedToken = readString( - config.get(`${endpoint ?? defaultEndpoint}.pat`), - ) + const storedToken = config.getToken(endpoint ?? defaultEndpoint) const envWorkspaceId = getWorkspaceIdFromEnv() - const storedWorkspaceId = readString(config.get('current_workspace_id')) + const storedWorkspaceId = config.getWorkspace() return { endpoint: endpoint ?? defaultEndpoint, @@ -70,7 +63,7 @@ export const resolveAuth = ( * shape it acts on, and how it may interact with the user. */ export interface CliContext { - config: ConfigStore + config: SeamConfig auth: AuthContext output: Output blueprint: ApiBlueprint @@ -78,11 +71,3 @@ export interface CliContext { /** The Seam API, constructed on first use and shared for the run. */ api: () => Promise } - -const readString = (value: unknown): string | null => { - if (typeof value !== 'string') return null - - const trimmedValue = value.trim() - - return trimmedValue === '' ? null : trimmedValue -} diff --git a/src/lib/interactions/endpoint-selection.ts b/src/lib/interactions/endpoint-selection.ts index 3c77ba31..2a9e05a6 100644 --- a/src/lib/interactions/endpoint-selection.ts +++ b/src/lib/interactions/endpoint-selection.ts @@ -1,11 +1,11 @@ import { assertMutable, selectEndpoint } from 'lib/auth/operations.js' -import { getConfigStore } from 'lib/config/index.js' +import { getConfig } from 'lib/config/index.js' import { resolveAuth } from 'lib/context.js' import { getOutput } from 'lib/output/get-output.js' import { promptAutocomplete } from 'lib/prompt.js' export async function interactForEndpointSelection() { - const config = getConfigStore() + const config = getConfig() assertMutable(resolveAuth(config), 'endpoint', 'select an endpoint') const endpoints = ['http://localhost:3020', 'https://connect.getseam.com'] diff --git a/src/lib/interactions/login.ts b/src/lib/interactions/login.ts index 066836b7..a3102ad7 100644 --- a/src/lib/interactions/login.ts +++ b/src/lib/interactions/login.ts @@ -3,7 +3,7 @@ import chalk from 'chalk' import { assertMutable, storeToken } from 'lib/auth/operations.js' import { validateToken } from 'lib/auth/validate-token.js' -import { getConfigStore } from 'lib/config/index.js' +import { getConfig } from 'lib/config/index.js' import { resolveAuth } from 'lib/context.js' import { getOutput } from 'lib/output/get-output.js' import { withLoading } from 'lib/output/with-loading.js' @@ -12,7 +12,7 @@ import { promptText } from 'lib/prompt.js' import { interactForWorkspaceId } from './workspace-id.js' export const interactForLogin = async () => { - const config = getConfigStore() + const config = getConfig() const output = getOutput() const auth = resolveAuth(config) diff --git a/src/lib/interactions/workspace-id.ts b/src/lib/interactions/workspace-id.ts index 45c886c7..8669a7d2 100644 --- a/src/lib/interactions/workspace-id.ts +++ b/src/lib/interactions/workspace-id.ts @@ -1,14 +1,14 @@ import { SeamHttpWithoutWorkspace } from '@seamapi/http/connect' import { assertMutable, selectWorkspace } from 'lib/auth/operations.js' -import { getConfigStore } from 'lib/config/index.js' +import { getConfig } from 'lib/config/index.js' import { resolveAuth } from 'lib/context.js' import { getSeamMultiWorkspace } from 'lib/http/client.js' import { withLoading } from 'lib/output/with-loading.js' import { promptAutocomplete } from 'lib/prompt.js' export const interactForWorkspaceId = async (personalAccessToken?: string) => { - const config = getConfigStore() + const config = getConfig() // Refuse before prompting: nothing selected here could be stored. assertMutable(resolveAuth(config), 'workspaceId', 'select a workspace') diff --git a/test/auth/operations.test.ts b/test/auth/operations.test.ts index adebde84..837a34a5 100644 --- a/test/auth/operations.test.ts +++ b/test/auth/operations.test.ts @@ -7,7 +7,11 @@ import { selectWorkspace, storeToken, } from 'lib/auth/operations.js' -import { createMemoryConfigStore } from 'lib/config/memory-config-store.js' +import { + createMemoryConfig, + createMemoryConfigStore, +} from 'lib/config/memory-config-store.js' +import { createSeamConfig } from 'lib/config/seam-config.js' import { endpointEnvVar, tokenEnvVar, workspaceIdEnvVar } from 'lib/env.js' const endpoint = 'https://connect.example.com' @@ -40,91 +44,93 @@ beforeEach(clearEnv) afterEach(clearEnv) test('login: stores a validated token under the current endpoint', async () => { - const store = createMemoryConfigStore({ endpoint }) + const config = createMemoryConfig({ endpoint }) const { validate, validated } = createValidate() - await login({ token: 'seam_apikey1_stored' }, store, validate) + await login({ token: 'seam_apikey1_stored' }, config, validate) expect(validated).toEqual([ { token: 'seam_apikey1_stored', workspaceId: undefined }, ]) - expect(store.get(`${endpoint}.pat`)).toBe('seam_apikey1_stored') + expect(config.getToken(endpoint)).toBe('seam_apikey1_stored') }) test('login: stores the token under an endpoint given alongside it', async () => { - const store = createMemoryConfigStore({ endpoint }) + const config = createMemoryConfig({ endpoint }) const { validate } = createValidate() await login( { endpoint: 'https://other.example.com', token: 'seam_apikey1_stored' }, - store, + config, validate, ) - expect(store.get('endpoint')).toBe('https://other.example.com') - expect(store.get('https://other.example.com.pat')).toBe('seam_apikey1_stored') - expect(store.has(`${endpoint}.pat`)).toBe(false) + expect(config.getEndpoint()).toBe('https://other.example.com') + expect(config.getToken('https://other.example.com')).toBe( + 'seam_apikey1_stored', + ) + expect(config.getToken(endpoint)).toBeNull() }) test('login: a new login clears the previous workspace selection', async () => { - const store = createMemoryConfigStore({ + const config = createMemoryConfig({ endpoint, current_workspace_id: 'workspace1', }) const { validate } = createValidate() - await login({ token: 'seam_apikey1_stored' }, store, validate) + await login({ token: 'seam_apikey1_stored' }, config, validate) - expect(store.has('current_workspace_id')).toBe(false) + expect(config.getWorkspace()).toBeNull() }) test('login: stores a workspace given with the token', async () => { - const store = createMemoryConfigStore({ endpoint }) + const config = createMemoryConfig({ endpoint }) const { validate, validated } = createValidate() await login( { token: 'seam_at1_stored', workspaceId: 'workspace1' }, - store, + config, validate, ) expect(validated).toEqual([ { token: 'seam_at1_stored', workspaceId: 'workspace1' }, ]) - expect(store.get('current_workspace_id')).toBe('workspace1') + expect(config.getWorkspace()).toBe('workspace1') }) test(`login: refuses while ${tokenEnvVar} is set, before storing anything`, async () => { process.env[tokenEnvVar] = 'seam_apikey1_env' - const store = createMemoryConfigStore({ endpoint }) + const config = createMemoryConfig({ endpoint }) const { validate, validated } = createValidate() await expect( - login({ token: 'seam_apikey1_stored' }, store, validate), + login({ token: 'seam_apikey1_stored' }, config, validate), ).rejects.toThrow(`Cannot log in while ${tokenEnvVar} is set`) - expect(store.has(`${endpoint}.pat`)).toBe(false) + expect(config.getToken(endpoint)).toBeNull() expect(validated).toEqual([]) }) test(`login: refuses an endpoint while ${endpointEnvVar} is set`, async () => { process.env[endpointEnvVar] = endpoint - const store = createMemoryConfigStore() + const config = createMemoryConfig() const { validate } = createValidate() await expect( - login({ endpoint: 'https://other.example.com' }, store, validate), + login({ endpoint: 'https://other.example.com' }, config, validate), ).rejects.toThrow(`Cannot select an endpoint while ${endpointEnvVar} is set`) }) test(`login: refuses a workspace while ${workspaceIdEnvVar} is set`, async () => { process.env[workspaceIdEnvVar] = 'workspace_env' - const store = createMemoryConfigStore({ endpoint }) + const config = createMemoryConfig({ endpoint }) const { validate } = createValidate() await expect( login( { token: 'seam_at1_stored', workspaceId: 'workspace1' }, - store, + config, validate, ), ).rejects.toThrow( @@ -133,11 +139,11 @@ test(`login: refuses a workspace while ${workspaceIdEnvVar} is set`, async () => }) test('storeToken: stores under the current endpoint without validating', () => { - const store = createMemoryConfigStore({ endpoint }) + const config = createMemoryConfig({ endpoint }) - storeToken('seam_apikey1_stored', store) + storeToken('seam_apikey1_stored', config) - expect(store.get(`${endpoint}.pat`)).toBe('seam_apikey1_stored') + expect(config.getToken(endpoint)).toBe('seam_apikey1_stored') }) test('logout: removes the stored token, legacy token, and workspace', () => { @@ -147,67 +153,70 @@ test('logout: removes the stored token, legacy token, and workspace', () => { pat: 'seam_apikey1_legacy', current_workspace_id: 'workspace1', }) + const config = createSeamConfig(store) - logout(store) + logout(config) - expect(store.has(`${endpoint}.pat`)).toBe(false) + expect(config.getToken(endpoint)).toBeNull() + expect(config.getWorkspace()).toBeNull() + // Nothing reads the un-namespaced token, so it is asserted where it lives. expect(store.has('pat')).toBe(false) - expect(store.has('current_workspace_id')).toBe(false) }) test(`logout: refuses while ${tokenEnvVar} is set`, () => { process.env[tokenEnvVar] = 'seam_apikey1_env' - const store = createMemoryConfigStore({ + const config = createMemoryConfig({ endpoint, [`${endpoint}.pat`]: 'seam_apikey1_stored', }) expect(() => { - logout(store) + logout(config) }).toThrow(`Cannot log out while ${tokenEnvVar} is set`) - expect(store.get(`${endpoint}.pat`)).toBe('seam_apikey1_stored') + expect(config.getToken(endpoint)).toBe('seam_apikey1_stored') }) test('selectEndpoint: stores the endpoint and clears the workspace', () => { - const store = createMemoryConfigStore({ current_workspace_id: 'workspace1' }) + const config = createMemoryConfig({ current_workspace_id: 'workspace1' }) - selectEndpoint(endpoint, store) + selectEndpoint(endpoint, config) - expect(store.get('endpoint')).toBe(endpoint) - expect(store.has('current_workspace_id')).toBe(false) + expect(config.getEndpoint()).toBe(endpoint) + expect(config.getWorkspace()).toBeNull() }) test('selectEndpoint: drops an endpoint left under the legacy key', () => { const store = createMemoryConfigStore({ server: 'https://old.example.com' }) + const config = createSeamConfig(store) - selectEndpoint(endpoint, store) + selectEndpoint(endpoint, config) - expect(store.get('endpoint')).toBe(endpoint) + expect(config.getEndpoint()).toBe(endpoint) expect(store.has('server')).toBe(false) }) test(`selectEndpoint: refuses while ${endpointEnvVar} is set`, () => { process.env[endpointEnvVar] = 'http://localhost:3020' - const store = createMemoryConfigStore() + const config = createMemoryConfig() expect(() => { - selectEndpoint(endpoint, store) + selectEndpoint(endpoint, config) }).toThrow(`Cannot select an endpoint while ${endpointEnvVar} is set`) }) test('selectWorkspace: stores the workspace selection', () => { - const store = createMemoryConfigStore() + const config = createMemoryConfig() - selectWorkspace('workspace1', store) + selectWorkspace('workspace1', config) - expect(store.get('current_workspace_id')).toBe('workspace1') + expect(config.getWorkspace()).toBe('workspace1') }) test(`selectWorkspace: refuses while ${workspaceIdEnvVar} is set`, () => { process.env[workspaceIdEnvVar] = 'workspace_env' - const store = createMemoryConfigStore() + const config = createMemoryConfig() expect(() => { - selectWorkspace('workspace1', store) + selectWorkspace('workspace1', config) }).toThrow(`Cannot select a workspace while ${workspaceIdEnvVar} is set`) }) diff --git a/test/context.test.ts b/test/context.test.ts index b4246ead..fc65b335 100644 --- a/test/context.test.ts +++ b/test/context.test.ts @@ -1,12 +1,12 @@ import { afterEach, beforeEach, expect, test } from 'vitest' -import { createMemoryConfigStore } from 'lib/config/memory-config-store.js' +import { createMemoryConfig } from 'lib/config/memory-config-store.js' import { resolveAuth } from 'lib/context.js' import { endpointEnvVar, tokenEnvVar, workspaceIdEnvVar } from 'lib/env.js' const endpoint = 'https://connect.example.com' -const store = createMemoryConfigStore +const config = createMemoryConfig const clearEnv = (): void => { delete process.env[endpointEnvVar] @@ -18,21 +18,21 @@ beforeEach(clearEnv) afterEach(clearEnv) test('resolveAuth: reads the stored endpoint', () => { - const auth = resolveAuth(store({ endpoint })) + const auth = resolveAuth(config({ endpoint })) expect(auth.endpoint).toBe(endpoint) expect(auth.endpointSource).toBe('config') }) test('resolveAuth: defaults the endpoint to Seam', () => { - const auth = resolveAuth(store()) + const auth = resolveAuth(config()) expect(auth.endpoint).toBe('https://connect.getseam.com') expect(auth.endpointSource).toBe('default') }) test('resolveAuth: reads an endpoint stored under the legacy key', () => { - const auth = resolveAuth(store({ server: endpoint })) + const auth = resolveAuth(config({ server: endpoint })) expect(auth.endpoint).toBe(endpoint) expect(auth.endpointSource).toBe('config') @@ -40,7 +40,7 @@ test('resolveAuth: reads an endpoint stored under the legacy key', () => { test('resolveAuth: the stored endpoint wins over the legacy key', () => { const auth = resolveAuth( - store({ endpoint, server: 'https://old.example.com' }), + config({ endpoint, server: 'https://old.example.com' }), ) expect(auth.endpoint).toBe(endpoint) @@ -49,7 +49,7 @@ test('resolveAuth: the stored endpoint wins over the legacy key', () => { test(`resolveAuth: ${endpointEnvVar} wins over the stored endpoint`, () => { process.env[endpointEnvVar] = 'http://localhost:3020' - const auth = resolveAuth(store({ endpoint })) + const auth = resolveAuth(config({ endpoint })) expect(auth.endpoint).toBe('http://localhost:3020') expect(auth.endpointSource).toBe('env') @@ -58,13 +58,13 @@ test(`resolveAuth: ${endpointEnvVar} wins over the stored endpoint`, () => { test(`resolveAuth: ${endpointEnvVar} is used without a stored endpoint`, () => { process.env[endpointEnvVar] = 'http://localhost:3020' - expect(resolveAuth(store()).endpoint).toBe('http://localhost:3020') + expect(resolveAuth(config()).endpoint).toBe('http://localhost:3020') }) test(`resolveAuth: ignores an empty ${endpointEnvVar}`, () => { process.env[endpointEnvVar] = '' - const auth = resolveAuth(store({ endpoint })) + const auth = resolveAuth(config({ endpoint })) expect(auth.endpoint).toBe(endpoint) expect(auth.endpointSource).toBe('config') @@ -72,7 +72,7 @@ test(`resolveAuth: ignores an empty ${endpointEnvVar}`, () => { test('resolveAuth: reads the token stored for the current endpoint', () => { const auth = resolveAuth( - store({ + config({ endpoint, [`${endpoint}.pat`]: 'seam_apikey1_stored', }), @@ -86,7 +86,7 @@ test(`resolveAuth: the token stored for ${endpointEnvVar} wins over the stored e process.env[endpointEnvVar] = 'http://localhost:3020' const auth = resolveAuth( - store({ + config({ endpoint, [`${endpoint}.pat`]: 'seam_apikey1_stored', 'http://localhost:3020.pat': 'seam_apikey1_local', @@ -100,7 +100,7 @@ test(`resolveAuth: ${tokenEnvVar} wins over the stored token`, () => { process.env[tokenEnvVar] = 'seam_apikey1_env' const auth = resolveAuth( - store({ + config({ endpoint, [`${endpoint}.pat`]: 'seam_apikey1_stored', }), @@ -113,14 +113,14 @@ test(`resolveAuth: ${tokenEnvVar} wins over the stored token`, () => { test(`resolveAuth: ${tokenEnvVar} is used without a stored token`, () => { process.env[tokenEnvVar] = 'seam_apikey1_env' - expect(resolveAuth(store()).token).toBe('seam_apikey1_env') + expect(resolveAuth(config()).token).toBe('seam_apikey1_env') }) test(`resolveAuth: ignores an empty ${tokenEnvVar}`, () => { process.env[tokenEnvVar] = ' ' const auth = resolveAuth( - store({ + config({ endpoint, [`${endpoint}.pat`]: 'seam_apikey1_stored', }), @@ -130,14 +130,14 @@ test(`resolveAuth: ignores an empty ${tokenEnvVar}`, () => { }) test('resolveAuth: token is null when nothing is set', () => { - const auth = resolveAuth(store()) + const auth = resolveAuth(config()) expect(auth.token).toBe(null) expect(auth.tokenSource).toBe(null) }) test('resolveAuth: reads the stored workspace selection', () => { - const auth = resolveAuth(store({ current_workspace_id: 'workspace1' })) + const auth = resolveAuth(config({ current_workspace_id: 'workspace1' })) expect(auth.workspaceId).toBe('workspace1') expect(auth.workspaceIdSource).toBe('config') @@ -146,7 +146,7 @@ test('resolveAuth: reads the stored workspace selection', () => { test(`resolveAuth: ${workspaceIdEnvVar} wins over the stored selection`, () => { process.env[workspaceIdEnvVar] = 'workspace2' - const auth = resolveAuth(store({ current_workspace_id: 'workspace1' })) + const auth = resolveAuth(config({ current_workspace_id: 'workspace1' })) expect(auth.workspaceId).toBe('workspace2') expect(auth.workspaceIdSource).toBe('env') @@ -155,19 +155,19 @@ test(`resolveAuth: ${workspaceIdEnvVar} wins over the stored selection`, () => { test(`resolveAuth: ${workspaceIdEnvVar} is used without a stored selection`, () => { process.env[workspaceIdEnvVar] = 'workspace2' - expect(resolveAuth(store()).workspaceId).toBe('workspace2') + expect(resolveAuth(config()).workspaceId).toBe('workspace2') }) test(`resolveAuth: ignores an empty ${workspaceIdEnvVar}`, () => { process.env[workspaceIdEnvVar] = '' expect( - resolveAuth(store({ current_workspace_id: 'workspace1' })).workspaceId, + resolveAuth(config({ current_workspace_id: 'workspace1' })).workspaceId, ).toBe('workspace1') }) test('resolveAuth: workspace is null when nothing is set', () => { - const auth = resolveAuth(store()) + const auth = resolveAuth(config()) expect(auth.workspaceId).toBe(null) expect(auth.workspaceIdSource).toBe(null) @@ -177,7 +177,7 @@ test('resolveAuth: each value resolves on its own', () => { process.env[workspaceIdEnvVar] = 'workspace2' const auth = resolveAuth( - store({ + config({ endpoint, [`${endpoint}.pat`]: 'seam_apikey1_stored', current_workspace_id: 'workspace1', From 2e2268e0fc4d869adcbc8a6779f887619c7845ae Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 04:50:29 +0000 Subject: [PATCH 2/2] Name it the CLI config, and name the keys where they are used Seam is not said internally unless it has to be, and the keys are read by this one file, so a constant for each was a name to look up rather than a literal to read. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01QCJ1v1NFc6b43GooAhij2c --- src/lib/auth/operations.ts | 14 ++--- .../config/{seam-config.ts => cli-config.ts} | 52 ++++++++----------- src/lib/config/config-store.ts | 12 ++--- src/lib/config/index.ts | 2 +- src/lib/config/memory-config-store.ts | 4 +- src/lib/config/values.ts | 15 +----- src/lib/context.ts | 6 +-- test/auth/operations.test.ts | 6 +-- 8 files changed, 46 insertions(+), 65 deletions(-) rename src/lib/config/{seam-config.ts => cli-config.ts} (53%) diff --git a/src/lib/auth/operations.ts b/src/lib/auth/operations.ts index 607f38bf..f1b43d12 100644 --- a/src/lib/auth/operations.ts +++ b/src/lib/auth/operations.ts @@ -1,4 +1,4 @@ -import { getConfig, type SeamConfig } from 'lib/config/index.js' +import { type CliConfig, getConfig } from 'lib/config/index.js' import { type AuthContext, resolveAuth } from 'lib/context.js' import { assertEnvVarUnset, @@ -60,7 +60,7 @@ export interface LoginOptions { */ export const login = async ( { endpoint, token, workspaceId }: LoginOptions, - config: SeamConfig = getConfig(), + config: CliConfig = getConfig(), validate: typeof validateToken = validateToken, ): Promise => { let auth = resolveAuth(config) @@ -92,7 +92,7 @@ export const login = async ( /** Store the token for the current endpoint, e.g., one just prompted for. */ export const storeToken = ( token: string, - config: SeamConfig = getConfig(), + config: CliConfig = getConfig(), ): void => { const auth = resolveAuth(config) assertMutable(auth, 'token', 'log in') @@ -100,7 +100,7 @@ export const storeToken = ( } /** Remove the stored token and workspace selection. */ -export const logout = (config: SeamConfig = getConfig()): void => { +export const logout = (config: CliConfig = getConfig()): void => { const auth = resolveAuth(config) assertMutable(auth, 'token', 'log out') config.unsetToken(auth.endpoint) @@ -114,7 +114,7 @@ export const logout = (config: SeamConfig = getConfig()): void => { */ export const selectEndpoint = ( endpoint: string, - config: SeamConfig = getConfig(), + config: CliConfig = getConfig(), ): void => { assertMutable(resolveAuth(config), 'endpoint', 'select an endpoint') config.setEndpoint(endpoint) @@ -123,7 +123,7 @@ export const selectEndpoint = ( /** Store the workspace requests are made against. */ export const selectWorkspace = ( workspaceId: string, - config: SeamConfig = getConfig(), + config: CliConfig = getConfig(), ): void => { assertMutable(resolveAuth(config), 'workspaceId', 'select a workspace') config.setWorkspace(workspaceId) @@ -132,7 +132,7 @@ export const selectWorkspace = ( /** Store whether API definitions come from the endpoint instead of npm. */ export const setUseRemoteApiDefs = ( useRemoteApiDefs: boolean, - config: SeamConfig = getConfig(), + config: CliConfig = getConfig(), ): void => { config.setUseRemoteApiDefs(useRemoteApiDefs) } diff --git a/src/lib/config/seam-config.ts b/src/lib/config/cli-config.ts similarity index 53% rename from src/lib/config/seam-config.ts rename to src/lib/config/cli-config.ts index 3d30d0e3..88acc271 100644 --- a/src/lib/config/seam-config.ts +++ b/src/lib/config/cli-config.ts @@ -1,20 +1,13 @@ import type { ConfigStore } from './config-store.js' -import { - currentWorkspaceIdKey, - endpointKey, - getTokenKey, - legacyEndpointKey, - patKey, - useRemoteApiDefsKey, -} from './values.js' + +/** Tokens are stored per endpoint, e.g. `https://connect.getseam.com.pat`. */ +const tokenKey = (endpoint: string): `${string}.pat` => `${endpoint}.pat` /** - * The CLI's configuration, in the terms the CLI thinks in. - * - * Every key lives in `values.ts` and is used here alone: a caller says what - * it wants stored, not where it goes or what it is called. + * The CLI's configuration, in the terms the CLI thinks in. Keys are named + * here alone: a caller says what it wants stored, not where it goes. */ -export interface SeamConfig { +export interface CliConfig { readonly path: string getEndpoint: () => string | null setEndpoint: (endpoint: string) => void @@ -28,57 +21,56 @@ export interface SeamConfig { setUseRemoteApiDefs: (useRemoteApiDefs: boolean) => void } -export const createSeamConfig = (store: ConfigStore): SeamConfig => ({ +export const createCliConfig = (store: ConfigStore): CliConfig => ({ get path() { return store.path }, getEndpoint: () => - readString(store.get(endpointKey)) ?? - readString(store.get(legacyEndpointKey)), + // `server` is what an older CLI called the endpoint. + readString(store.get('endpoint')) ?? readString(store.get('server')), /** * Store the endpoint, dropping what belonged to the previous one: the * workspace selection, and any value left under the legacy key that - * {@link SeamConfig.getEndpoint} would otherwise still fall back to. + * {@link CliConfig.getEndpoint} would otherwise still fall back to. */ setEndpoint: (endpoint) => { - store.set(endpointKey, endpoint) - store.delete(legacyEndpointKey) - store.delete(currentWorkspaceIdKey) + store.set('endpoint', endpoint) + store.delete('server') + store.delete('current_workspace_id') }, - /** Tokens are stored per endpoint, so one is never sent to another. */ - getToken: (endpoint) => readString(store.get(getTokenKey(endpoint))), + getToken: (endpoint) => readString(store.get(tokenKey(endpoint))), setToken: (endpoint, token) => { - store.set(getTokenKey(endpoint), token) + store.set(tokenKey(endpoint), token) }, unsetToken: (endpoint) => { - store.delete(getTokenKey(endpoint)) + store.delete(tokenKey(endpoint)) // Configs written before tokens were stored per endpoint may still hold // an un-namespaced one. - store.delete(patKey) + store.delete('pat') }, - getWorkspace: () => readString(store.get(currentWorkspaceIdKey)), + getWorkspace: () => readString(store.get('current_workspace_id')), setWorkspace: (workspaceId) => { - store.set(currentWorkspaceIdKey, workspaceId) + store.set('current_workspace_id', workspaceId) }, unsetWorkspace: () => { - store.delete(currentWorkspaceIdKey) + store.delete('current_workspace_id') }, getUseRemoteApiDefs: () => { - const useRemoteApiDefs = store.get(useRemoteApiDefsKey) + const useRemoteApiDefs = store.get('use_remote_api_defs') return typeof useRemoteApiDefs === 'boolean' ? useRemoteApiDefs : null }, setUseRemoteApiDefs: (useRemoteApiDefs) => { - store.set(useRemoteApiDefsKey, useRemoteApiDefs) + store.set('use_remote_api_defs', useRemoteApiDefs) }, }) diff --git a/src/lib/config/config-store.ts b/src/lib/config/config-store.ts index 64ee8d4a..9fef04df 100644 --- a/src/lib/config/config-store.ts +++ b/src/lib/config/config-store.ts @@ -3,14 +3,14 @@ import { join } from 'node:path' import Configstore from 'configstore' import envPaths from 'env-paths' +import { type CliConfig, createCliConfig } from './cli-config.js' import { migrateConfigStore } from './migrate.js' -import { createSeamConfig, type SeamConfig } from './seam-config.js' import { isStateKey, mergeConfig, splitConfig } from './values.js' const configFileName = 'cli.json' const legacyConfigStoreId = 'seam-cli' -/** Every directory Seam keeps files in, for the CLI and for what it mounts. */ +/** Every directory the CLI keeps files in, and what it mounts keeps its own. */ export const rootPaths = envPaths('seam', { suffix: '' }) /** @@ -31,15 +31,15 @@ export interface ConfigStore { clear: () => void } -let config: SeamConfig | null = null +let config: CliConfig | null = null -export const getConfig = (): SeamConfig => { - config ??= createSeamConfig(createConfigStore()) +export const getConfig = (): CliConfig => { + config ??= createCliConfig(createConfigStore()) return config } /** Replace the config, e.g., with an in-memory one for a test. */ -export const setConfig = (nextConfig: SeamConfig): void => { +export const setConfig = (nextConfig: CliConfig): void => { config = nextConfig } diff --git a/src/lib/config/index.ts b/src/lib/config/index.ts index afd703b3..89ea419e 100644 --- a/src/lib/config/index.ts +++ b/src/lib/config/index.ts @@ -1,3 +1,4 @@ +export { type CliConfig, createCliConfig } from './cli-config.js' export { type ConfigStore, getConfig, @@ -10,4 +11,3 @@ export { createMemoryConfig, createMemoryConfigStore, } from './memory-config-store.js' -export { createSeamConfig, type SeamConfig } from './seam-config.js' diff --git a/src/lib/config/memory-config-store.ts b/src/lib/config/memory-config-store.ts index 0dbfe923..9eaa8f9e 100644 --- a/src/lib/config/memory-config-store.ts +++ b/src/lib/config/memory-config-store.ts @@ -1,5 +1,5 @@ +import { type CliConfig, createCliConfig } from './cli-config.js' import type { ConfigStore } from './config-store.js' -import { createSeamConfig, type SeamConfig } from './seam-config.js' /** * A real {@link ConfigStore} held in memory, for tests. @@ -68,4 +68,4 @@ export const createMemoryConfigStore = ( */ export const createMemoryConfig = ( initialValues: Record = {}, -): SeamConfig => createSeamConfig(createMemoryConfigStore(initialValues)) +): CliConfig => createCliConfig(createMemoryConfigStore(initialValues)) diff --git a/src/lib/config/values.ts b/src/lib/config/values.ts index d7b611e4..89931261 100644 --- a/src/lib/config/values.ts +++ b/src/lib/config/values.ts @@ -4,19 +4,8 @@ * Pure transforms shared by the persistent store and the legacy migration. */ -export const endpointKey = 'endpoint' - -/** What the endpoint was called before, still read by `getEndpoint`. */ -export const legacyEndpointKey = 'server' - -export const currentWorkspaceIdKey = 'current_workspace_id' - -export const patKey = 'pat' - -export const useRemoteApiDefsKey = 'use_remote_api_defs' - -/** Tokens are stored per endpoint, e.g. `https://connect.getseam.com.pat`. */ -export const getTokenKey = (endpoint: string): string => `${endpoint}.${patKey}` +const currentWorkspaceIdKey = 'current_workspace_id' +const patKey = 'pat' /** Whether a key holds auth state rather than a setting. */ export const isStateKey = (key: string): boolean => { diff --git a/src/lib/context.ts b/src/lib/context.ts index cf2147d4..443f6436 100644 --- a/src/lib/context.ts +++ b/src/lib/context.ts @@ -1,6 +1,6 @@ import type { Interactivity } from './args/parse.js' import type { ApiBlueprint } from './blueprint/index.js' -import { getConfig, type SeamConfig } from './config/index.js' +import { type CliConfig, getConfig } from './config/index.js' import { getEndpointFromEnv, getTokenFromEnv, @@ -30,7 +30,7 @@ export interface AuthContext { workspaceIdSource: Exclude | null } -export const resolveAuth = (config: SeamConfig = getConfig()): AuthContext => { +export const resolveAuth = (config: CliConfig = getConfig()): AuthContext => { const envEndpoint = getEndpointFromEnv() const storedEndpoint = config.getEndpoint() const endpoint = envEndpoint ?? storedEndpoint @@ -63,7 +63,7 @@ export const resolveAuth = (config: SeamConfig = getConfig()): AuthContext => { * shape it acts on, and how it may interact with the user. */ export interface CliContext { - config: SeamConfig + config: CliConfig auth: AuthContext output: Output blueprint: ApiBlueprint diff --git a/test/auth/operations.test.ts b/test/auth/operations.test.ts index 837a34a5..bc92032f 100644 --- a/test/auth/operations.test.ts +++ b/test/auth/operations.test.ts @@ -7,11 +7,11 @@ import { selectWorkspace, storeToken, } from 'lib/auth/operations.js' +import { createCliConfig } from 'lib/config/cli-config.js' import { createMemoryConfig, createMemoryConfigStore, } from 'lib/config/memory-config-store.js' -import { createSeamConfig } from 'lib/config/seam-config.js' import { endpointEnvVar, tokenEnvVar, workspaceIdEnvVar } from 'lib/env.js' const endpoint = 'https://connect.example.com' @@ -153,7 +153,7 @@ test('logout: removes the stored token, legacy token, and workspace', () => { pat: 'seam_apikey1_legacy', current_workspace_id: 'workspace1', }) - const config = createSeamConfig(store) + const config = createCliConfig(store) logout(config) @@ -187,7 +187,7 @@ test('selectEndpoint: stores the endpoint and clears the workspace', () => { test('selectEndpoint: drops an endpoint left under the legacy key', () => { const store = createMemoryConfigStore({ server: 'https://old.example.com' }) - const config = createSeamConfig(store) + const config = createCliConfig(store) selectEndpoint(endpoint, config)