Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions src/lib/adapter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { afterEach, expect, test, vi } from 'vitest'

import {
createMemoryAdapter,
defaultEndpoint,
getAdapter,
getAuth,
loadAuth,
resetAdapter,
setAdapter,
type WizardAuth,
} from './adapter.js'

afterEach(resetAdapter)

const apiKeyLogin: WizardAuth = {
endpoint: 'https://connect.example.com',
apiKey: 'seam_apikey1_token',
workspaceId: 'workspace-1',
}

test('adapter: runs logged out against an in-memory adapter by default', async () => {
expect(await loadAuth()).toEqual({
endpoint: defaultEndpoint,
apiKey: null,
workspaceId: null,
})
})

test('adapter: keeps values for the run and nothing beyond it', async () => {
await getAdapter().config.set('sdk', 'python')
expect(await getAdapter().config.get('sdk')).toBe('python')

resetAdapter()
expect(await getAdapter().config.get('sdk')).toBeUndefined()
})

test('adapter: keeps config apart from state', async () => {
await getAdapter().config.set('sdk', 'python')

expect(await getAdapter().state.get('sdk')).toBeUndefined()
})

test('adapter: uses the auth the host answers with', async () => {
setAdapter(createMemoryAdapter({ auth: apiKeyLogin }))

expect(await loadAuth()).toEqual(apiKeyLogin)
expect(getAuth()).toEqual(apiKeyLogin)
})

test('adapter: asks the adapter who the developer is once', async () => {
const getAuthSpy = vi.fn(async () => apiKeyLogin)
setAdapter({ ...createMemoryAdapter(), getAuth: getAuthSpy })

await loadAuth()
await loadAuth()

expect(getAuthSpy).toHaveBeenCalledTimes(1)
})

test('adapter: is logged out until the auth has been loaded', () => {
setAdapter(createMemoryAdapter({ auth: apiKeyLogin }))

expect(getAuth().apiKey).toBeNull()
expect(getAuth().endpoint).toBe(defaultEndpoint)
})

test('adapter: forgets the auth loaded for a previous adapter', async () => {
setAdapter(createMemoryAdapter({ auth: apiKeyLogin }))
await loadAuth()

setAdapter(createMemoryAdapter())

expect(await loadAuth()).toMatchObject({ apiKey: null })
})

test('adapter: starts from the values it was created with', async () => {
setAdapter(
createMemoryAdapter({
config: { sdk: 'javascript' },
state: { 'projects.app-1234567890': { goal: 'Set up Seam.' } },
}),
)

expect(await getAdapter().config.get('sdk')).toBe('javascript')
expect(await getAdapter().state.get('projects.app-1234567890')).toEqual({
goal: 'Set up Seam.',
})
})
72 changes: 72 additions & 0 deletions src/lib/adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
export interface WizardAuth {
endpoint: string
apiKey: string | null
workspaceId: string | null
}

export interface StorageAdapter {
get: (key: string) => Promise<unknown>
set: (key: string, value: unknown) => Promise<void>
}

export interface WizardAdapter {
getAuth: () => Promise<WizardAuth>
config: StorageAdapter
state: StorageAdapter
}

export const defaultEndpoint = 'https://connect.getseam.com'

const loggedOut: WizardAuth = {
endpoint: defaultEndpoint,
apiKey: null,
workspaceId: null,
}

export const createMemoryStorage = (
initialValues: Record<string, unknown> = {},
): StorageAdapter => {
const values = new Map(Object.entries(initialValues))
return {
get: async (key) => values.get(key),
set: async (key, value) => {
values.set(key, value)
},
}
}

export const createMemoryAdapter = ({
auth = loggedOut,
config = {},
state = {},
}: {
auth?: WizardAuth
config?: Record<string, unknown>
state?: Record<string, unknown>
} = {}): WizardAdapter => ({
getAuth: async () => auth,
config: createMemoryStorage(config),
state: createMemoryStorage(state),
})

let adapter: WizardAdapter = createMemoryAdapter()
let auth: WizardAuth | null = null

export const getAdapter = (): WizardAdapter => adapter

export const setAdapter = (nextAdapter: WizardAdapter): void => {
adapter = nextAdapter
auth = null
}

export const resetAdapter = (): void => {
adapter = createMemoryAdapter()
auth = null
}

export const loadAuth = async (): Promise<WizardAuth> => {
auth ??= await adapter.getAuth()
return auth
}

export const getAuth = (): WizardAuth => auth ?? loggedOut
5 changes: 5 additions & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
export {
type StorageAdapter,
type WizardAdapter,
type WizardAuth,
} from './adapter.js'
export { default, type WizardOptions } from './wizard.js'
40 changes: 39 additions & 1 deletion src/lib/wizard.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { beforeEach, expect, test, vi } from 'vitest'
import { afterEach, beforeEach, expect, test, vi } from 'vitest'

import { createMemoryAdapter, getAuth, resetAdapter } from './adapter.js'
import { renderApp } from './render.js'
import seamapiWizardVersion from './version.js'
import wizard from './wizard.js'
Expand All @@ -10,6 +11,8 @@ beforeEach(() => {
vi.mocked(renderApp).mockClear()
})

afterEach(resetAdapter)

const captureOutput = async (
options: Parameters<typeof wizard>[0],
): Promise<string> => {
Expand Down Expand Up @@ -70,3 +73,38 @@ test('wizard: runs the app in the given directory', async () => {
await wizard({ argv: [], cwd: '/tmp/example-project' })
expect(renderApp).toHaveBeenCalledWith({ root: '/tmp/example-project' })
})

test('wizard: runs on the adapter it is given', async () => {
const adapter = createMemoryAdapter({
auth: {
endpoint: 'https://connect.example.com',
apiKey: 'seam_apikey1_token',
workspaceId: 'workspace-1',
},
})

await wizard({ argv: [], adapter })

expect(getAuth()).toMatchObject({ endpoint: 'https://connect.example.com' })
})

test('wizard: runs logged out when it is given no adapter', async () => {
await wizard({ argv: [] })

expect(getAuth().apiKey).toBeNull()
})

test('wizard: asks no adapter anything to display usage', async () => {
const getAuthSpy = vi.fn(async () => ({
endpoint: 'https://connect.example.com',
apiKey: null,
workspaceId: null,
}))

await captureOutput({
argv: ['--help'],
adapter: { ...createMemoryAdapter(), getAuth: getAuthSpy },
})

expect(getAuthSpy).not.toHaveBeenCalled()
})
6 changes: 6 additions & 0 deletions src/lib/wizard.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import parseArgs from 'minimist'

import { loadAuth, setAdapter, type WizardAdapter } from './adapter.js'
import { renderApp } from './render.js'
import seamapiWizardVersion from './version.js'

Expand Down Expand Up @@ -29,6 +30,8 @@ export interface WizardOptions {
* `.seam/onboarding.json`.
*/
cwd?: string

adapter?: WizardAdapter
}

/**
Expand All @@ -43,6 +46,7 @@ export interface WizardOptions {
*/
const wizard = async (options: WizardOptions = {}): Promise<void> => {
const { argv = [], commandName = 'wizard', cwd = process.cwd() } = options
if (options.adapter != null) setAdapter(options.adapter)

const args = parseArgs([...argv], {
boolean: ['help', 'version'],
Expand All @@ -59,6 +63,8 @@ const wizard = async (options: WizardOptions = {}): Promise<void> => {
return
}

await loadAuth()

await renderApp({ root: cwd })
}

Expand Down
Loading