@marcusrbrown/infra 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,136 @@
1
+ /// <reference types="bun" />
2
+
3
+ import {isCancel, multiselect, select, text} from '@clack/prompts'
4
+ import {z} from 'zod'
5
+
6
+ import {cancelAndExit} from './prompts'
7
+
8
+ const providerIdSchema = z.enum(['anthropic', 'openai'])
9
+ export type ProviderId = z.infer<typeof providerIdSchema>
10
+
11
+ const MODEL_ID_RE = /^(?:anthropic|openai)\/[a-z\d](?:[a-z\d.\-]*[a-z\d])?$/
12
+
13
+ /**
14
+ * Parse a comma-separated provider list string into a validated ProviderId array.
15
+ * Rejects empty input, unknown providers, and duplicates.
16
+ */
17
+ export function parseProviders(input: string): ProviderId[] {
18
+ const parts = input
19
+ .split(',')
20
+ .map(p => p.trim())
21
+ .filter(Boolean)
22
+
23
+ if (parts.length === 0) {
24
+ throw new Error('--providers must not be empty. Supported values: anthropic, openai')
25
+ }
26
+
27
+ const parsed = parts.map(p => {
28
+ const result = providerIdSchema.safeParse(p)
29
+ if (!result.success) {
30
+ throw new Error(`Unknown provider "${p}". Supported values: anthropic, openai`)
31
+ }
32
+ return result.data
33
+ })
34
+
35
+ const deduped = new Set(parsed)
36
+ if (deduped.size < parsed.length) {
37
+ throw new Error(`--providers contains duplicate values: ${parsed.join(',')}`)
38
+ }
39
+
40
+ return parsed
41
+ }
42
+
43
+ export const PROVIDER_DEFAULTS: Record<ProviderId, string> = {
44
+ anthropic: 'anthropic/claude-sonnet-4-6',
45
+ openai: 'openai/gpt-5.4-mini',
46
+ }
47
+
48
+ const CUSTOM_MODEL_SENTINEL = '__custom__'
49
+
50
+ /**
51
+ * Interactively prompt the user to select one or more providers.
52
+ * Anthropic is pre-checked. Empty selection re-prompts.
53
+ */
54
+ export async function promptForProviders(): Promise<ProviderId[]> {
55
+ let providers: ProviderId[] = []
56
+
57
+ do {
58
+ const result = await multiselect<ProviderId>({
59
+ message: 'Select providers to configure',
60
+ options: [
61
+ {value: 'anthropic', label: 'Anthropic'},
62
+ {value: 'openai', label: 'OpenAI'},
63
+ ],
64
+ initialValues: ['anthropic'],
65
+ required: false,
66
+ })
67
+
68
+ if (isCancel(result)) {
69
+ cancelAndExit('Setup cancelled before selecting providers.')
70
+ }
71
+
72
+ providers = result as ProviderId[]
73
+ } while (providers.length === 0)
74
+
75
+ return providers
76
+ }
77
+
78
+ /**
79
+ * Interactively prompt the user to select a default model.
80
+ * When only one provider is selected, returns that provider's default immediately.
81
+ * When multiple providers are selected, shows a select with preset options and a custom entry.
82
+ */
83
+ export async function promptForModel(providers: ProviderId[]): Promise<string> {
84
+ if (providers.length === 1) {
85
+ return PROVIDER_DEFAULTS[providers[0] as ProviderId]
86
+ }
87
+
88
+ const chosen = await select<string>({
89
+ message: 'Choose a default model',
90
+ options: [
91
+ {value: 'openai/gpt-5.4-mini', label: 'openai/gpt-5.4-mini'},
92
+ {value: 'anthropic/claude-sonnet-4-6', label: 'anthropic/claude-sonnet-4-6'},
93
+ {value: CUSTOM_MODEL_SENTINEL, label: 'Enter custom model ID...'},
94
+ ],
95
+ })
96
+
97
+ if (isCancel(chosen)) {
98
+ cancelAndExit('Setup cancelled before selecting a model.')
99
+ }
100
+
101
+ if (chosen === CUSTOM_MODEL_SENTINEL) {
102
+ return promptForCustomModel()
103
+ }
104
+
105
+ return chosen as string
106
+ }
107
+
108
+ async function promptForCustomModel(): Promise<string> {
109
+ let modelId: string | undefined
110
+
111
+ do {
112
+ const result = await text({
113
+ message: 'Enter a custom model ID (e.g. openai/gpt-5.4-mini)',
114
+ placeholder: 'provider/model-name',
115
+ validate: value => {
116
+ if (!MODEL_ID_RE.test(value ?? '')) {
117
+ return 'Model ID must match provider/model-name (lowercase, digits, dots, hyphens only)'
118
+ }
119
+ return undefined
120
+ },
121
+ })
122
+
123
+ if (isCancel(result)) {
124
+ cancelAndExit('Setup cancelled before entering a custom model ID.')
125
+ }
126
+
127
+ const candidate = result as string
128
+ if (MODEL_ID_RE.test(candidate)) {
129
+ modelId = candidate
130
+ }
131
+ // If the mock bypasses clack's internal validate and returns a bad value,
132
+ // loop again to re-prompt.
133
+ } while (!modelId)
134
+
135
+ return modelId
136
+ }