@marcusrbrown/infra 0.13.1 → 0.13.3
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.
- package/dist/cli.js +11965 -0
- package/package.json +7 -3
- package/src/__snapshots__/cli.test.ts.snap +0 -219
- package/src/__test__/mcp-ctx-fixture.test.ts +0 -108
- package/src/__test__/mcp-ctx-fixture.ts +0 -104
- package/src/cli.test.ts +0 -66
- package/src/cli.ts +0 -43
- package/src/commands/cliproxy/config.test.ts +0 -475
- package/src/commands/cliproxy/config.ts +0 -238
- package/src/commands/cliproxy/deploy.test.ts +0 -181
- package/src/commands/cliproxy/deploy.ts +0 -157
- package/src/commands/cliproxy/host.test.ts +0 -73
- package/src/commands/cliproxy/host.ts +0 -31
- package/src/commands/cliproxy/index.ts +0 -21
- package/src/commands/cliproxy/keys.test.ts +0 -326
- package/src/commands/cliproxy/keys.ts +0 -201
- package/src/commands/cliproxy/login.test.ts +0 -415
- package/src/commands/cliproxy/login.ts +0 -131
- package/src/commands/cliproxy/models.test.ts +0 -592
- package/src/commands/cliproxy/models.ts +0 -237
- package/src/commands/cliproxy/open.test.ts +0 -132
- package/src/commands/cliproxy/open.ts +0 -78
- package/src/commands/cliproxy/setup/gh.test.ts +0 -218
- package/src/commands/cliproxy/setup/gh.ts +0 -250
- package/src/commands/cliproxy/setup/preview.test.ts +0 -159
- package/src/commands/cliproxy/setup/preview.ts +0 -41
- package/src/commands/cliproxy/setup/prompts.test.ts +0 -58
- package/src/commands/cliproxy/setup/prompts.ts +0 -99
- package/src/commands/cliproxy/setup/providers.test.ts +0 -245
- package/src/commands/cliproxy/setup/providers.ts +0 -136
- package/src/commands/cliproxy/setup/smoke-test.test.ts +0 -821
- package/src/commands/cliproxy/setup/smoke-test.ts +0 -223
- package/src/commands/cliproxy/setup/templates.test.ts +0 -357
- package/src/commands/cliproxy/setup/templates.ts +0 -145
- package/src/commands/cliproxy/setup/validation.test.ts +0 -567
- package/src/commands/cliproxy/setup/validation.ts +0 -209
- package/src/commands/cliproxy/setup/workflow-analyzer.test.ts +0 -363
- package/src/commands/cliproxy/setup/workflow-analyzer.ts +0 -136
- package/src/commands/cliproxy/setup.test.ts +0 -2366
- package/src/commands/cliproxy/setup.ts +0 -809
- package/src/commands/cliproxy/shared.test.ts +0 -118
- package/src/commands/cliproxy/shared.ts +0 -84
- package/src/commands/cliproxy/status.test.ts +0 -1157
- package/src/commands/cliproxy/status.ts +0 -597
- package/src/commands/dashboard/deploy.test.ts +0 -215
- package/src/commands/dashboard/deploy.ts +0 -135
- package/src/commands/dashboard/host.test.ts +0 -79
- package/src/commands/dashboard/host.ts +0 -33
- package/src/commands/dashboard/index.ts +0 -13
- package/src/commands/dashboard/logs.test.ts +0 -317
- package/src/commands/dashboard/logs.ts +0 -180
- package/src/commands/dashboard/status.test.ts +0 -708
- package/src/commands/dashboard/status.ts +0 -293
- package/src/commands/gateway/backup.test.ts +0 -426
- package/src/commands/gateway/backup.ts +0 -205
- package/src/commands/gateway/deploy.test.ts +0 -491
- package/src/commands/gateway/deploy.ts +0 -173
- package/src/commands/gateway/host.test.ts +0 -89
- package/src/commands/gateway/host.ts +0 -33
- package/src/commands/gateway/index.ts +0 -17
- package/src/commands/gateway/logs.test.ts +0 -263
- package/src/commands/gateway/logs.ts +0 -160
- package/src/commands/gateway/restore.test.ts +0 -650
- package/src/commands/gateway/restore.ts +0 -308
- package/src/commands/gateway/status.test.ts +0 -755
- package/src/commands/gateway/status.ts +0 -255
- package/src/commands/keeweb/deploy.test.ts +0 -177
- package/src/commands/keeweb/deploy.ts +0 -191
- package/src/commands/keeweb/index.ts +0 -11
- package/src/commands/keeweb/open.test.ts +0 -54
- package/src/commands/keeweb/open.ts +0 -30
- package/src/commands/keeweb/status.test.ts +0 -509
- package/src/commands/keeweb/status.ts +0 -359
- package/src/commands/mcp.test.ts +0 -260
- package/src/commands/mcp.ts +0 -58
- package/src/commands/status.test.ts +0 -222
- package/src/commands/status.ts +0 -161
- package/src/commands/umami/deploy.test.ts +0 -202
- package/src/commands/umami/deploy.ts +0 -132
- package/src/commands/umami/host.test.ts +0 -79
- package/src/commands/umami/host.ts +0 -33
- package/src/commands/umami/index.ts +0 -13
- package/src/commands/umami/logs.test.ts +0 -191
- package/src/commands/umami/logs.ts +0 -163
- package/src/commands/umami/status.test.ts +0 -633
- package/src/commands/umami/status.ts +0 -285
- package/src/commands/vpn/client.test.ts +0 -949
- package/src/commands/vpn/client.ts +0 -467
- package/src/commands/vpn/deploy.test.ts +0 -208
- package/src/commands/vpn/deploy.ts +0 -143
- package/src/commands/vpn/host.test.ts +0 -89
- package/src/commands/vpn/host.ts +0 -36
- package/src/commands/vpn/index.ts +0 -15
- package/src/commands/vpn/logs.test.ts +0 -234
- package/src/commands/vpn/logs.ts +0 -136
- package/src/commands/vpn/peers.test.ts +0 -478
- package/src/commands/vpn/shared.test.ts +0 -454
- package/src/commands/vpn/shared.ts +0 -170
- package/src/commands/vpn/status.test.ts +0 -142
- package/src/commands/vpn/status.ts +0 -78
- package/src/conventions.test.ts +0 -1180
- package/src/lib/action-ctx.ts +0 -25
- package/src/lib/known-hosts.test.ts +0 -192
- package/src/lib/known-hosts.ts +0 -76
- package/src/lib/redact.ts +0 -21
- package/src/lib/ssh-identity.test.ts +0 -134
- package/src/lib/ssh-identity.ts +0 -76
- /package/{src/resources/known_hosts → dist/known_hosts-tk724cp0.} +0 -0
|
@@ -1,245 +0,0 @@
|
|
|
1
|
-
/// <reference types="bun" />
|
|
2
|
-
|
|
3
|
-
import type {MultiSelectOptions, TextOptions} from '@clack/prompts'
|
|
4
|
-
import {describe, expect, it, spyOn} from 'bun:test'
|
|
5
|
-
|
|
6
|
-
import {parseProviders, promptForModel, promptForProviders} from './providers'
|
|
7
|
-
|
|
8
|
-
// Type helper: cast a concrete-typed clack implementation to the generic spy type.
|
|
9
|
-
// clack's multiselect/text/select are generic functions; Bun's spyOn preserves the
|
|
10
|
-
// generic signature, so mockImplementation requires the same generic. We provide a
|
|
11
|
-
// concrete instantiation and widen through `unknown` — this is safe because the
|
|
12
|
-
// concrete type is a structural subtype of the generic at the call site.
|
|
13
|
-
function asMultiselectImpl<V>(fn: (opts: MultiSelectOptions<V>) => Promise<V[] | symbol>) {
|
|
14
|
-
return fn as unknown as <Value>(opts: MultiSelectOptions<Value>) => Promise<Value[] | symbol>
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function asTextImpl(fn: (opts: TextOptions) => Promise<string | symbol>) {
|
|
18
|
-
return fn as unknown as (opts: TextOptions) => Promise<string | symbol>
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
describe('option parsing', () => {
|
|
22
|
-
describe('parseProviders', () => {
|
|
23
|
-
it("parses \"anthropic,openai\" to ['anthropic', 'openai']", () => {
|
|
24
|
-
expect(parseProviders('anthropic,openai')).toEqual(['anthropic', 'openai'])
|
|
25
|
-
})
|
|
26
|
-
|
|
27
|
-
it('parses "openai" to [\'openai\']', () => {
|
|
28
|
-
expect(parseProviders('openai')).toEqual(['openai'])
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
it('parses "anthropic" to [\'anthropic\']', () => {
|
|
32
|
-
expect(parseProviders('anthropic')).toEqual(['anthropic'])
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('rejects duplicate providers with a "duplicate" error', () => {
|
|
36
|
-
expect(() => parseProviders('anthropic,anthropic')).toThrow(/duplicate/i)
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
it('rejects an empty string with a clear message', () => {
|
|
40
|
-
expect(() => parseProviders('')).toThrow()
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
it('rejects an unknown provider "claude" with an enum error', () => {
|
|
44
|
-
expect(() => parseProviders('claude')).toThrow()
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
it('trims whitespace around provider names', () => {
|
|
48
|
-
expect(parseProviders(' anthropic , openai ')).toEqual(['anthropic', 'openai'])
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it('rejects "__proto__" (prototype-chain safety)', () => {
|
|
52
|
-
expect(() => parseProviders('__proto__')).toThrow()
|
|
53
|
-
})
|
|
54
|
-
})
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
describe('interactive provider/model prompts', () => {
|
|
58
|
-
// We spy on @clack/prompts functions directly since Bun's mock.module
|
|
59
|
-
// requires static hoisting. Instead we use spyOn on the imported module.
|
|
60
|
-
// The helpers call the clack functions via the module binding, so we
|
|
61
|
-
// intercept them via spyOn after importing.
|
|
62
|
-
|
|
63
|
-
// Note: Because providers.ts imports clack at module load time and calls the
|
|
64
|
-
// functions directly (not via a re-exported object), we need to use
|
|
65
|
-
// mock.module to intercept. However, Bun's mock.module must be called
|
|
66
|
-
// before the module is imported. Since providers.ts is already imported above,
|
|
67
|
-
// we test the helpers by injecting controlled behavior through the clack
|
|
68
|
-
// module mock at the describe level using beforeEach/afterEach with spyOn
|
|
69
|
-
// on the actual clack module exports.
|
|
70
|
-
//
|
|
71
|
-
// The approach: import clack directly and spyOn its exports.
|
|
72
|
-
|
|
73
|
-
describe('promptForProviders', () => {
|
|
74
|
-
it('happy path: anthropic-only selection returns [anthropic]', async () => {
|
|
75
|
-
const clack = await import('@clack/prompts')
|
|
76
|
-
// multiselect<Value> returns Promise<Value[] | symbol>; resolved value is string[] | symbol
|
|
77
|
-
const multiselectSpy = spyOn(clack, 'multiselect').mockResolvedValue(['anthropic'])
|
|
78
|
-
|
|
79
|
-
const result = await promptForProviders()
|
|
80
|
-
|
|
81
|
-
expect(result).toEqual(['anthropic'])
|
|
82
|
-
expect(multiselectSpy).toHaveBeenCalledTimes(1)
|
|
83
|
-
|
|
84
|
-
multiselectSpy.mockRestore()
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
it('happy path: both providers selected returns [anthropic, openai]', async () => {
|
|
88
|
-
const clack = await import('@clack/prompts')
|
|
89
|
-
const multiselectSpy = spyOn(clack, 'multiselect').mockResolvedValue(['anthropic', 'openai'])
|
|
90
|
-
|
|
91
|
-
const result = await promptForProviders()
|
|
92
|
-
|
|
93
|
-
expect(result).toEqual(['anthropic', 'openai'])
|
|
94
|
-
|
|
95
|
-
multiselectSpy.mockRestore()
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
it('edge case: empty selection re-prompts; multiselect called exactly twice', async () => {
|
|
99
|
-
const clack = await import('@clack/prompts')
|
|
100
|
-
let callCount = 0
|
|
101
|
-
const multiselectSpy = spyOn(clack, 'multiselect').mockImplementation(
|
|
102
|
-
asMultiselectImpl<string>(async () => {
|
|
103
|
-
callCount++
|
|
104
|
-
if (callCount === 1) return []
|
|
105
|
-
return ['anthropic']
|
|
106
|
-
}),
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
const result = await promptForProviders()
|
|
110
|
-
|
|
111
|
-
expect(result).toEqual(['anthropic'])
|
|
112
|
-
expect(multiselectSpy).toHaveBeenCalledTimes(2)
|
|
113
|
-
|
|
114
|
-
multiselectSpy.mockRestore()
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
it('edge case: cancel mid-flow causes process.exit(0)', async () => {
|
|
118
|
-
const clack = await import('@clack/prompts')
|
|
119
|
-
const cancelSymbol = Symbol('cancel')
|
|
120
|
-
const multiselectSpy = spyOn(clack, 'multiselect').mockResolvedValue(cancelSymbol)
|
|
121
|
-
const isCancelSpy = spyOn(clack, 'isCancel').mockImplementation(v => v === cancelSymbol)
|
|
122
|
-
const cancelSpy = spyOn(clack, 'cancel').mockImplementation(() => {})
|
|
123
|
-
const exitSpy = spyOn(process, 'exit').mockImplementation((_code?: number): never => {
|
|
124
|
-
throw new Error('process.exit called')
|
|
125
|
-
})
|
|
126
|
-
|
|
127
|
-
await expect(promptForProviders()).rejects.toThrow('process.exit called')
|
|
128
|
-
|
|
129
|
-
multiselectSpy.mockRestore()
|
|
130
|
-
isCancelSpy.mockRestore()
|
|
131
|
-
cancelSpy.mockRestore()
|
|
132
|
-
exitSpy.mockRestore()
|
|
133
|
-
})
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
describe('promptForModel', () => {
|
|
137
|
-
it('happy path: single anthropic provider returns anthropic/claude-sonnet-4-6 without prompting', async () => {
|
|
138
|
-
const clack = await import('@clack/prompts')
|
|
139
|
-
const selectSpy = spyOn(clack, 'select')
|
|
140
|
-
|
|
141
|
-
const result = await promptForModel(['anthropic'])
|
|
142
|
-
|
|
143
|
-
expect(result).toBe('anthropic/claude-sonnet-4-6')
|
|
144
|
-
expect(selectSpy).not.toHaveBeenCalled()
|
|
145
|
-
|
|
146
|
-
selectSpy.mockRestore()
|
|
147
|
-
})
|
|
148
|
-
|
|
149
|
-
it('happy path: single openai provider returns openai/gpt-5.4-mini without prompting', async () => {
|
|
150
|
-
const clack = await import('@clack/prompts')
|
|
151
|
-
const selectSpy = spyOn(clack, 'select')
|
|
152
|
-
|
|
153
|
-
const result = await promptForModel(['openai'])
|
|
154
|
-
|
|
155
|
-
expect(result).toBe('openai/gpt-5.4-mini')
|
|
156
|
-
expect(selectSpy).not.toHaveBeenCalled()
|
|
157
|
-
|
|
158
|
-
selectSpy.mockRestore()
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
it('happy path: both providers, operator picks openai/gpt-5.4-mini from select', async () => {
|
|
162
|
-
const clack = await import('@clack/prompts')
|
|
163
|
-
const selectSpy = spyOn(clack, 'select').mockResolvedValue('openai/gpt-5.4-mini')
|
|
164
|
-
|
|
165
|
-
const result = await promptForModel(['anthropic', 'openai'])
|
|
166
|
-
|
|
167
|
-
expect(result).toBe('openai/gpt-5.4-mini')
|
|
168
|
-
expect(selectSpy).toHaveBeenCalledTimes(1)
|
|
169
|
-
|
|
170
|
-
selectSpy.mockRestore()
|
|
171
|
-
})
|
|
172
|
-
|
|
173
|
-
it('happy path: both providers, operator picks anthropic/claude-sonnet-4-6 from select', async () => {
|
|
174
|
-
const clack = await import('@clack/prompts')
|
|
175
|
-
const selectSpy = spyOn(clack, 'select').mockResolvedValue('anthropic/claude-sonnet-4-6')
|
|
176
|
-
|
|
177
|
-
const result = await promptForModel(['anthropic', 'openai'])
|
|
178
|
-
|
|
179
|
-
expect(result).toBe('anthropic/claude-sonnet-4-6')
|
|
180
|
-
|
|
181
|
-
selectSpy.mockRestore()
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
it('happy path: operator picks "enter custom..." then types openai/gpt-5.4-mini', async () => {
|
|
185
|
-
const clack = await import('@clack/prompts')
|
|
186
|
-
const selectSpy = spyOn(clack, 'select').mockResolvedValue('__custom__')
|
|
187
|
-
const textSpy = spyOn(clack, 'text').mockResolvedValue('openai/gpt-5.4-mini')
|
|
188
|
-
|
|
189
|
-
const result = await promptForModel(['anthropic', 'openai'])
|
|
190
|
-
|
|
191
|
-
expect(result).toBe('openai/gpt-5.4-mini')
|
|
192
|
-
expect(textSpy).toHaveBeenCalledTimes(1)
|
|
193
|
-
|
|
194
|
-
selectSpy.mockRestore()
|
|
195
|
-
textSpy.mockRestore()
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
it('edge case: custom model entry fails regex then succeeds on second attempt', async () => {
|
|
199
|
-
const clack = await import('@clack/prompts')
|
|
200
|
-
const selectSpy = spyOn(clack, 'select').mockResolvedValue('__custom__')
|
|
201
|
-
let textCallCount = 0
|
|
202
|
-
const textSpy = spyOn(clack, 'text').mockImplementation(
|
|
203
|
-
asTextImpl(async () => {
|
|
204
|
-
textCallCount++
|
|
205
|
-
// Simulate the validate function being called inline by the mock
|
|
206
|
-
// The real clack text prompt calls validate internally; here we just
|
|
207
|
-
// return the value and let the helper's validate logic re-prompt.
|
|
208
|
-
// Since we can't simulate clack's internal validate loop, we test
|
|
209
|
-
// that the helper's validate function rejects bad input.
|
|
210
|
-
if (textCallCount === 1) {
|
|
211
|
-
// Return a bad value — the helper should detect this and re-prompt
|
|
212
|
-
return 'bad-model'
|
|
213
|
-
}
|
|
214
|
-
return 'openai/gpt-5.4-mini'
|
|
215
|
-
}),
|
|
216
|
-
)
|
|
217
|
-
|
|
218
|
-
const result = await promptForModel(['anthropic', 'openai'])
|
|
219
|
-
|
|
220
|
-
expect(result).toBe('openai/gpt-5.4-mini')
|
|
221
|
-
expect(textSpy.mock.calls.length).toBeGreaterThanOrEqual(1)
|
|
222
|
-
|
|
223
|
-
selectSpy.mockRestore()
|
|
224
|
-
textSpy.mockRestore()
|
|
225
|
-
})
|
|
226
|
-
|
|
227
|
-
it('edge case: cancel during model select causes process.exit(0)', async () => {
|
|
228
|
-
const clack = await import('@clack/prompts')
|
|
229
|
-
const cancelSymbol = Symbol('cancel')
|
|
230
|
-
const selectSpy = spyOn(clack, 'select').mockResolvedValue(cancelSymbol)
|
|
231
|
-
const isCancelSpy = spyOn(clack, 'isCancel').mockImplementation(v => v === cancelSymbol)
|
|
232
|
-
const cancelSpy = spyOn(clack, 'cancel').mockImplementation(() => {})
|
|
233
|
-
const exitSpy = spyOn(process, 'exit').mockImplementation((_code?: number): never => {
|
|
234
|
-
throw new Error('process.exit called')
|
|
235
|
-
})
|
|
236
|
-
|
|
237
|
-
await expect(promptForModel(['anthropic', 'openai'])).rejects.toThrow('process.exit called')
|
|
238
|
-
|
|
239
|
-
selectSpy.mockRestore()
|
|
240
|
-
isCancelSpy.mockRestore()
|
|
241
|
-
cancelSpy.mockRestore()
|
|
242
|
-
exitSpy.mockRestore()
|
|
243
|
-
})
|
|
244
|
-
})
|
|
245
|
-
})
|
|
@@ -1,136 +0,0 @@
|
|
|
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
|
-
}
|