@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,809 +0,0 @@
|
|
|
1
|
-
/// <reference types="bun" />
|
|
2
|
-
|
|
3
|
-
import type {goke} from 'goke'
|
|
4
|
-
|
|
5
|
-
import type {ActionCtx} from '../../lib/action-ctx'
|
|
6
|
-
|
|
7
|
-
import {cancel, confirm, intro, log, note, outro, select, text} from '@clack/prompts'
|
|
8
|
-
import {z} from 'zod'
|
|
9
|
-
|
|
10
|
-
import {resolveManagementKey} from './config'
|
|
11
|
-
import {
|
|
12
|
-
applyGhValue,
|
|
13
|
-
assertGhAuthenticated,
|
|
14
|
-
assertGhInstalled,
|
|
15
|
-
assertRepoAccess,
|
|
16
|
-
createManagementApiKey,
|
|
17
|
-
deleteManagementApiKey,
|
|
18
|
-
listExistingGhNames,
|
|
19
|
-
withGhRetry,
|
|
20
|
-
withSpinner,
|
|
21
|
-
} from './setup/gh'
|
|
22
|
-
import {formatDryRunPreview} from './setup/preview'
|
|
23
|
-
import {buildApiKeyValue, cancelAndExit, ensureRepoFormat, promptGenericSecretNames, promptValue} from './setup/prompts'
|
|
24
|
-
import {parseProviders, promptForModel, promptForProviders, PROVIDER_DEFAULTS, type ProviderId} from './setup/providers'
|
|
25
|
-
import {runSmokeTest} from './setup/smoke-test'
|
|
26
|
-
import {
|
|
27
|
-
collectCollisions,
|
|
28
|
-
formatTemplateSummary,
|
|
29
|
-
getHarnessTemplate,
|
|
30
|
-
harnessSchema,
|
|
31
|
-
stripTrailingSlash,
|
|
32
|
-
type Harness,
|
|
33
|
-
type HarnessTemplate,
|
|
34
|
-
} from './setup/templates'
|
|
35
|
-
import {
|
|
36
|
-
assertProxyKeyWorks,
|
|
37
|
-
assertProxyReachable,
|
|
38
|
-
MODEL_ID_RE,
|
|
39
|
-
validateSetupOptions,
|
|
40
|
-
verifyModelsAvailable,
|
|
41
|
-
} from './setup/validation'
|
|
42
|
-
import {checkFroBotWorkflow, formatWorkflowSnippet} from './setup/workflow-analyzer'
|
|
43
|
-
|
|
44
|
-
export {formatDryRunPreview, type DryRunPreviewOptions} from './setup/preview'
|
|
45
|
-
export {validateSetupOptions, verifyModelsAvailable} from './setup/validation'
|
|
46
|
-
|
|
47
|
-
const DEFAULT_CLIPROXY_URL = 'https://cliproxy.fro.bot'
|
|
48
|
-
|
|
49
|
-
export interface SetupOptions {
|
|
50
|
-
key?: string
|
|
51
|
-
repo?: string
|
|
52
|
-
harness?: Harness
|
|
53
|
-
/** Raw comma-separated provider list string (e.g. "anthropic,openai"). Use parseProviders() to validate. */
|
|
54
|
-
providers?: string
|
|
55
|
-
model?: string
|
|
56
|
-
force?: boolean
|
|
57
|
-
dryRun?: boolean
|
|
58
|
-
verifySmoke?: boolean
|
|
59
|
-
ackKeyReuse?: boolean
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
interface SetupPlan {
|
|
63
|
-
repo: string
|
|
64
|
-
harness: Harness
|
|
65
|
-
keyValue: string
|
|
66
|
-
keyName?: string
|
|
67
|
-
createKey: boolean
|
|
68
|
-
template: HarnessTemplate
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Internal: test-only DI surface. Not part of the published API.
|
|
72
|
-
export interface RunSetupDeps {
|
|
73
|
-
interactive?: boolean
|
|
74
|
-
baseUrl?: string
|
|
75
|
-
ctx?: ActionCtx
|
|
76
|
-
resolveManagementKey?: typeof resolveManagementKey
|
|
77
|
-
gh?: {
|
|
78
|
-
assertGhInstalled: typeof assertGhInstalled
|
|
79
|
-
assertGhAuthenticated: typeof assertGhAuthenticated
|
|
80
|
-
assertRepoAccess: typeof assertRepoAccess
|
|
81
|
-
listExistingGhNames: typeof listExistingGhNames
|
|
82
|
-
createManagementApiKey: typeof createManagementApiKey
|
|
83
|
-
deleteManagementApiKey: typeof deleteManagementApiKey
|
|
84
|
-
applyGhValue: typeof applyGhValue
|
|
85
|
-
withGhRetry: typeof withGhRetry
|
|
86
|
-
}
|
|
87
|
-
prompts?: {
|
|
88
|
-
promptValue: typeof promptValue
|
|
89
|
-
confirm: typeof confirm
|
|
90
|
-
intro: typeof intro
|
|
91
|
-
note: typeof note
|
|
92
|
-
outro: typeof outro
|
|
93
|
-
promptForProviders?: typeof promptForProviders
|
|
94
|
-
promptForModel?: typeof promptForModel
|
|
95
|
-
}
|
|
96
|
-
smoke?: {
|
|
97
|
-
runSmokeTest: typeof runSmokeTest
|
|
98
|
-
}
|
|
99
|
-
validation?: {
|
|
100
|
-
assertProxyReachable: typeof assertProxyReachable
|
|
101
|
-
assertProxyKeyWorks: typeof assertProxyKeyWorks
|
|
102
|
-
verifyModelsAvailable: typeof verifyModelsAvailable
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function resolveBaseUrl(input?: string): string {
|
|
107
|
-
return stripTrailingSlash(input ?? process.env.CLIPROXY_URL ?? DEFAULT_CLIPROXY_URL)
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Emit a warning without ever throwing. Used inside verifyWrittenNamesVisible so that a
|
|
112
|
-
* failure in warning emission itself cannot escape to the outer write catch and wrongly
|
|
113
|
-
* roll back a key whose secrets are already written.
|
|
114
|
-
*/
|
|
115
|
-
function safeWarn(message: string): void {
|
|
116
|
-
try {
|
|
117
|
-
log.warn(message)
|
|
118
|
-
} catch {
|
|
119
|
-
// Warning emission must never escape the post-write readback — a throw here would
|
|
120
|
-
// reach the outer write catch and wrongly roll back a key whose secrets are written.
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
function buildCannotVerifyMessage(repo: string, writtenSecretNames: string[], writtenVariableNames: string[]): string {
|
|
125
|
-
const secretPart =
|
|
126
|
-
writtenSecretNames.length > 0
|
|
127
|
-
? `gh secret list --repo ${repo} (expect: ${writtenSecretNames.join(', ')})`
|
|
128
|
-
: `gh secret list --repo ${repo}`
|
|
129
|
-
const variablePart =
|
|
130
|
-
writtenVariableNames.length > 0
|
|
131
|
-
? `gh variable list --repo ${repo} (expect: ${writtenVariableNames.join(', ')})`
|
|
132
|
-
: `gh variable list --repo ${repo}`
|
|
133
|
-
return (
|
|
134
|
-
`Post-write readback: could not verify the written names are visible in ${repo} ` +
|
|
135
|
-
`(the GitHub list call failed). Verify manually: ${secretPart}; ${variablePart}.`
|
|
136
|
-
)
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* After a successful write, re-list secret and variable names and warn if any written name
|
|
141
|
-
* is absent on readback — signaling an unreliable token list view that may have bypassed
|
|
142
|
-
* the pre-write safety gates.
|
|
143
|
-
*
|
|
144
|
-
* Distinguishes:
|
|
145
|
-
* - Verified mismatch: readback succeeded but a written name is absent (strong signal).
|
|
146
|
-
* - Cannot verify: the readback gh call itself failed (weaker signal).
|
|
147
|
-
*
|
|
148
|
-
* NEVER throws. The entire body is wrapped in a single try/catch so any failure — including
|
|
149
|
-
* errors during diff computation or warning emission — degrades to the cannot-verify warning.
|
|
150
|
-
* A throw here would propagate to the mutationError rollback and wrongly delete a key whose
|
|
151
|
-
* secrets are already written.
|
|
152
|
-
*/
|
|
153
|
-
async function verifyWrittenNamesVisible(
|
|
154
|
-
repo: string,
|
|
155
|
-
writtenSecretNames: string[],
|
|
156
|
-
writtenVariableNames: string[],
|
|
157
|
-
listExistingGhNames: typeof import('./setup/gh').listExistingGhNames,
|
|
158
|
-
): Promise<void> {
|
|
159
|
-
try {
|
|
160
|
-
let secretReadback: string[]
|
|
161
|
-
let variableReadback: string[]
|
|
162
|
-
let readbackFailed = false
|
|
163
|
-
|
|
164
|
-
try {
|
|
165
|
-
secretReadback = await listExistingGhNames(repo, 'secret')
|
|
166
|
-
} catch {
|
|
167
|
-
readbackFailed = true
|
|
168
|
-
secretReadback = []
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
if (readbackFailed) {
|
|
172
|
-
variableReadback = []
|
|
173
|
-
} else {
|
|
174
|
-
try {
|
|
175
|
-
variableReadback = await listExistingGhNames(repo, 'variable')
|
|
176
|
-
} catch {
|
|
177
|
-
readbackFailed = true
|
|
178
|
-
variableReadback = []
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
if (readbackFailed) {
|
|
183
|
-
safeWarn(buildCannotVerifyMessage(repo, writtenSecretNames, writtenVariableNames))
|
|
184
|
-
return
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const absentSecrets = writtenSecretNames.filter(name => !secretReadback.includes(name))
|
|
188
|
-
const absentVariables = writtenVariableNames.filter(name => !variableReadback.includes(name))
|
|
189
|
-
|
|
190
|
-
if (absentSecrets.length === 0 && absentVariables.length === 0) {
|
|
191
|
-
// Happy path: all written names are visible. Emit nothing.
|
|
192
|
-
return
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
const lines: string[] = [
|
|
196
|
-
`Post-write readback: the following written names are not visible in ${repo} — ` +
|
|
197
|
-
`the token's list view may be unreliable and the pre-write safety gates may have been bypassed.`,
|
|
198
|
-
]
|
|
199
|
-
|
|
200
|
-
if (absentSecrets.length > 0) {
|
|
201
|
-
lines.push(` Absent secrets: ${absentSecrets.join(', ')}`)
|
|
202
|
-
lines.push(` Verify manually: gh secret list --repo ${repo}`)
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if (absentVariables.length > 0) {
|
|
206
|
-
lines.push(` Absent variables: ${absentVariables.join(', ')}`)
|
|
207
|
-
lines.push(` Verify manually: gh variable list --repo ${repo}`)
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
safeWarn(lines.join('\n'))
|
|
211
|
-
} catch {
|
|
212
|
-
// Any failure in the entire verification block (readback, diff, or warning emission)
|
|
213
|
-
// degrades to this softer cannot-verify warning. Never re-throw.
|
|
214
|
-
safeWarn(buildCannotVerifyMessage(repo, writtenSecretNames, writtenVariableNames))
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function extractErrorMessage(error: unknown): string {
|
|
219
|
-
return error instanceof Error ? error.message : String(error)
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Redact a bearer token for display in interactive prompts — never show raw key values.
|
|
223
|
-
// Exported for direct unit testing of the redaction contract. The redacted form is
|
|
224
|
-
// what gets shown in the interactive key-reuse prompt; the raw key must never reach the prompt UI.
|
|
225
|
-
export function redactKey(key: string): string {
|
|
226
|
-
if (key.length < 12) return 'sk-***'
|
|
227
|
-
return `${key.slice(0, 3)}***${key.slice(-4)}`
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
async function buildInteractivePlan(
|
|
231
|
-
options: SetupOptions,
|
|
232
|
-
baseUrl: string,
|
|
233
|
-
promptsImpl: Required<RunSetupDeps>['prompts'],
|
|
234
|
-
): Promise<SetupPlan> {
|
|
235
|
-
const createKey = !options.key
|
|
236
|
-
const keyName = createKey
|
|
237
|
-
? await promptsImpl.promptValue(
|
|
238
|
-
text({
|
|
239
|
-
message: 'Name this new CLIProxyAPI key',
|
|
240
|
-
placeholder: 'my-repo-ci',
|
|
241
|
-
validate: value => ((value ?? '').trim().length > 0 ? undefined : 'Key name is required.'),
|
|
242
|
-
}),
|
|
243
|
-
'Setup cancelled before naming the key.',
|
|
244
|
-
)
|
|
245
|
-
: undefined
|
|
246
|
-
|
|
247
|
-
const harness =
|
|
248
|
-
options.harness ??
|
|
249
|
-
(await promptsImpl.promptValue(
|
|
250
|
-
select<Harness>({
|
|
251
|
-
message: 'Choose the harness to configure',
|
|
252
|
-
options: [
|
|
253
|
-
{value: 'opencode', label: 'OpenCode'},
|
|
254
|
-
{value: 'claude-code', label: 'Claude Code'},
|
|
255
|
-
{value: 'generic', label: 'Generic'},
|
|
256
|
-
],
|
|
257
|
-
}),
|
|
258
|
-
'Setup cancelled before selecting a harness.',
|
|
259
|
-
))
|
|
260
|
-
|
|
261
|
-
const repo = options.repo
|
|
262
|
-
? ensureRepoFormat(options.repo)
|
|
263
|
-
: ensureRepoFormat(
|
|
264
|
-
await promptsImpl.promptValue(
|
|
265
|
-
text({
|
|
266
|
-
message: 'Target GitHub repository',
|
|
267
|
-
placeholder: 'owner/repo',
|
|
268
|
-
validate: value => {
|
|
269
|
-
try {
|
|
270
|
-
ensureRepoFormat(value ?? '')
|
|
271
|
-
return undefined
|
|
272
|
-
} catch (error) {
|
|
273
|
-
return extractErrorMessage(error)
|
|
274
|
-
}
|
|
275
|
-
},
|
|
276
|
-
}),
|
|
277
|
-
'Setup cancelled before choosing a repository.',
|
|
278
|
-
),
|
|
279
|
-
)
|
|
280
|
-
|
|
281
|
-
let providers: ProviderId[] | undefined
|
|
282
|
-
let model: string | undefined
|
|
283
|
-
|
|
284
|
-
if (harness === 'opencode') {
|
|
285
|
-
const doPromptForProviders = promptsImpl.promptForProviders ?? promptForProviders
|
|
286
|
-
const doPromptForModel = promptsImpl.promptForModel ?? promptForModel
|
|
287
|
-
providers = await doPromptForProviders()
|
|
288
|
-
model = await doPromptForModel(providers)
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
const keyValue = options.key ?? buildApiKeyValue(keyName ?? 'cliproxy')
|
|
292
|
-
const genericSecretNames = harness === 'generic' ? await promptGenericSecretNames() : undefined
|
|
293
|
-
|
|
294
|
-
return {
|
|
295
|
-
repo,
|
|
296
|
-
harness,
|
|
297
|
-
keyValue,
|
|
298
|
-
keyName,
|
|
299
|
-
createKey,
|
|
300
|
-
template: getHarnessTemplate(harness, {keyValue, baseUrl, genericSecretNames, providers, model}),
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* Returns true when the provider list includes anything beyond anthropic-only.
|
|
306
|
-
* Anthropic-only repos see no behavior change (G7 invariant).
|
|
307
|
-
*/
|
|
308
|
-
export function requiresDestructiveProviderChangeConfirmation(providers: ProviderId[]): boolean {
|
|
309
|
-
return !(providers.length === 1 && providers[0] === 'anthropic')
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Deprecated: use requiresDestructiveProviderChangeConfirmation. Will be removed in a future major.
|
|
313
|
-
export const mustConfirmDestructive = requiresDestructiveProviderChangeConfirmation
|
|
314
|
-
|
|
315
|
-
export async function buildNonInteractivePlan(
|
|
316
|
-
options: SetupOptions,
|
|
317
|
-
baseUrl: string,
|
|
318
|
-
deps?: Pick<RunSetupDeps, 'validation'>,
|
|
319
|
-
): Promise<SetupPlan> {
|
|
320
|
-
const harness = harnessSchema.parse(options.harness)
|
|
321
|
-
const repo = ensureRepoFormat(options.repo ?? '')
|
|
322
|
-
const keyValue = options.key ?? ''
|
|
323
|
-
|
|
324
|
-
const providers: ProviderId[] = options.providers ? parseProviders(options.providers) : ['anthropic']
|
|
325
|
-
|
|
326
|
-
let model: string
|
|
327
|
-
if (options.model) {
|
|
328
|
-
model = options.model
|
|
329
|
-
} else if (providers.length === 1) {
|
|
330
|
-
model = PROVIDER_DEFAULTS[providers[0] as ProviderId]
|
|
331
|
-
} else {
|
|
332
|
-
// Unreachable: validateSetupOptions enforces model when providers.length > 1
|
|
333
|
-
throw new Error('Pass --model <provider/model-id> when selecting multiple providers.')
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// --dry-run: skip verifyModelsAvailable and force check; return plan for preview
|
|
337
|
-
if (options.dryRun) {
|
|
338
|
-
return {
|
|
339
|
-
repo,
|
|
340
|
-
harness,
|
|
341
|
-
keyValue,
|
|
342
|
-
createKey: false,
|
|
343
|
-
template: getHarnessTemplate(harness, {keyValue: keyValue || 'sk-placeholder', baseUrl, providers, model}),
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
const verifyModels = deps?.validation?.verifyModelsAvailable ?? verifyModelsAvailable
|
|
348
|
-
|
|
349
|
-
// Destructive overwrite gate: non-anthropic-only requires --force in non-interactive mode.
|
|
350
|
-
// Check BEFORE verifyModelsAvailable to avoid a network call when the gate will reject anyway.
|
|
351
|
-
if (requiresDestructiveProviderChangeConfirmation(providers) && !options.force) {
|
|
352
|
-
throw new Error(
|
|
353
|
-
`Refusing destructive provider change on ${options.repo ?? ''} without --force. Selected providers ${providers.join(', ')} would overwrite existing GitHub secret values (OPENCODE_AUTH_JSON, OPENCODE_CONFIG, FRO_BOT_MODEL). Note: --force authorizes overwriting these GitHub secret values; it does NOT rotate the underlying CLIProxyAPI proxy bearer token (which is preserved byte-for-byte when --key is supplied).`,
|
|
354
|
-
)
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
await verifyModels(baseUrl, keyValue, providers, model)
|
|
358
|
-
|
|
359
|
-
return {
|
|
360
|
-
repo,
|
|
361
|
-
harness,
|
|
362
|
-
keyValue,
|
|
363
|
-
createKey: false,
|
|
364
|
-
template: getHarnessTemplate(harness, {keyValue, baseUrl, providers, model}),
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
// Default real implementations for DI
|
|
369
|
-
const realGh: Required<RunSetupDeps>['gh'] = {
|
|
370
|
-
assertGhInstalled,
|
|
371
|
-
assertGhAuthenticated,
|
|
372
|
-
assertRepoAccess,
|
|
373
|
-
listExistingGhNames,
|
|
374
|
-
createManagementApiKey,
|
|
375
|
-
deleteManagementApiKey,
|
|
376
|
-
applyGhValue,
|
|
377
|
-
withGhRetry,
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
const realPrompts: Required<RunSetupDeps>['prompts'] = {
|
|
381
|
-
promptValue,
|
|
382
|
-
confirm,
|
|
383
|
-
intro,
|
|
384
|
-
note,
|
|
385
|
-
outro,
|
|
386
|
-
promptForProviders,
|
|
387
|
-
promptForModel,
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
const realSmoke: Required<RunSetupDeps>['smoke'] = {
|
|
391
|
-
runSmokeTest,
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
const realValidation: Required<RunSetupDeps>['validation'] = {
|
|
395
|
-
assertProxyReachable,
|
|
396
|
-
assertProxyKeyWorks,
|
|
397
|
-
verifyModelsAvailable,
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
const realCtx: ActionCtx = {
|
|
401
|
-
console: {
|
|
402
|
-
log: (...args: unknown[]) => {
|
|
403
|
-
console.log(...args)
|
|
404
|
-
},
|
|
405
|
-
error: (...args: unknown[]) => {
|
|
406
|
-
console.error(...args)
|
|
407
|
-
},
|
|
408
|
-
},
|
|
409
|
-
process: {
|
|
410
|
-
stdout: {write: (chunk: string) => process.stdout.write(chunk)},
|
|
411
|
-
stderr: {write: (chunk: string) => process.stderr.write(chunk)},
|
|
412
|
-
exit: (code: number) => process.exit(code),
|
|
413
|
-
},
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
// Internal: test-only DI surface. Not part of the published API.
|
|
417
|
-
export async function runSetupCommand(options: SetupOptions, deps: RunSetupDeps = {}): Promise<void> {
|
|
418
|
-
const interactive = deps.interactive ?? Boolean(process.stdin.isTTY)
|
|
419
|
-
const baseUrl = deps.baseUrl ?? resolveBaseUrl()
|
|
420
|
-
// ctxInjected distinguishes MCP-mounted callers (which need ctx.console.error to surface errors
|
|
421
|
-
// through the MCP transport) from bare CLI callers (where cli.ts top-level catch already logs).
|
|
422
|
-
// Without this distinction, CLI users see the error twice.
|
|
423
|
-
const ctxInjected = deps.ctx !== undefined
|
|
424
|
-
const ctx = deps.ctx ?? realCtx
|
|
425
|
-
const gh = deps.gh ?? realGh
|
|
426
|
-
const prompts = deps.prompts ?? realPrompts
|
|
427
|
-
const smoke = deps.smoke ?? realSmoke
|
|
428
|
-
const validation = deps.validation ?? realValidation
|
|
429
|
-
const mgmtKeyResolver = deps.resolveManagementKey ?? resolveManagementKey
|
|
430
|
-
|
|
431
|
-
// --dry-run: short-circuit before validation so it works with no flags.
|
|
432
|
-
// Never blocks on stdin — safe to run anywhere.
|
|
433
|
-
if (options.dryRun) {
|
|
434
|
-
const providers: ProviderId[] = options.providers ? parseProviders(options.providers) : ['anthropic']
|
|
435
|
-
const model = options.model ?? PROVIDER_DEFAULTS[providers[0] as ProviderId]
|
|
436
|
-
const harness = options.harness ?? 'opencode'
|
|
437
|
-
const repo = options.repo ?? '<repo not specified>'
|
|
438
|
-
const keyValue = options.key ?? 'sk-placeholder'
|
|
439
|
-
ctx.console.log(
|
|
440
|
-
formatDryRunPreview({
|
|
441
|
-
repo,
|
|
442
|
-
harness,
|
|
443
|
-
providers,
|
|
444
|
-
model,
|
|
445
|
-
template: getHarnessTemplate(harness, {keyValue, baseUrl, providers, model}),
|
|
446
|
-
}),
|
|
447
|
-
)
|
|
448
|
-
return
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
validateSetupOptions(options, interactive)
|
|
452
|
-
|
|
453
|
-
if (interactive) {
|
|
454
|
-
prompts.intro('CLIProxyAPI setup wizard')
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
try {
|
|
458
|
-
await withSpinner('Checking GitHub CLI availability', async () => {
|
|
459
|
-
await gh.assertGhInstalled()
|
|
460
|
-
await gh.assertGhAuthenticated()
|
|
461
|
-
})
|
|
462
|
-
|
|
463
|
-
await withSpinner('Checking CLIProxyAPI reachability', async () => {
|
|
464
|
-
await validation.assertProxyReachable(baseUrl)
|
|
465
|
-
})
|
|
466
|
-
|
|
467
|
-
const plan = interactive
|
|
468
|
-
? await buildInteractivePlan(options, baseUrl, prompts)
|
|
469
|
-
: await buildNonInteractivePlan(options, baseUrl, {validation})
|
|
470
|
-
|
|
471
|
-
if (plan.createKey) {
|
|
472
|
-
mgmtKeyResolver()
|
|
473
|
-
}
|
|
474
|
-
|
|
475
|
-
await gh.withGhRetry(
|
|
476
|
-
`Checking GitHub access for ${plan.repo}`,
|
|
477
|
-
async () => {
|
|
478
|
-
await gh.assertRepoAccess(plan.repo)
|
|
479
|
-
},
|
|
480
|
-
interactive,
|
|
481
|
-
)
|
|
482
|
-
|
|
483
|
-
if (options.key) {
|
|
484
|
-
log.info('Using the provided API key value directly. No new CLIProxyAPI key will be created.')
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
if (interactive) {
|
|
488
|
-
prompts.note(
|
|
489
|
-
[
|
|
490
|
-
`Proxy: ${baseUrl}`,
|
|
491
|
-
`Repository: ${plan.repo}`,
|
|
492
|
-
`Harness: ${plan.harness}`,
|
|
493
|
-
plan.createKey ? `New key name: ${plan.keyName}` : 'Using existing key value',
|
|
494
|
-
'GitHub values to write:',
|
|
495
|
-
formatTemplateSummary(plan.template),
|
|
496
|
-
].join('\n'),
|
|
497
|
-
'Setup summary',
|
|
498
|
-
)
|
|
499
|
-
|
|
500
|
-
const shouldContinue = await prompts.promptValue(
|
|
501
|
-
prompts.confirm({
|
|
502
|
-
message: 'Proceed with GitHub secret and variable updates?',
|
|
503
|
-
active: 'yes',
|
|
504
|
-
inactive: 'no',
|
|
505
|
-
initialValue: true,
|
|
506
|
-
}),
|
|
507
|
-
'Setup cancelled before applying GitHub values.',
|
|
508
|
-
)
|
|
509
|
-
|
|
510
|
-
if (!shouldContinue) {
|
|
511
|
-
cancelAndExit('No changes applied.')
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
const [existingSecrets, existingVariables] = await gh.withGhRetry(
|
|
516
|
-
'Checking existing GitHub secrets and variables',
|
|
517
|
-
async () =>
|
|
518
|
-
Promise.all([gh.listExistingGhNames(plan.repo, 'secret'), gh.listExistingGhNames(plan.repo, 'variable')]),
|
|
519
|
-
interactive,
|
|
520
|
-
)
|
|
521
|
-
|
|
522
|
-
// Key-reuse acknowledgment guard: bearer token must not appear in prompt text
|
|
523
|
-
if (options.key && existingSecrets.includes('OPENCODE_AUTH_JSON')) {
|
|
524
|
-
if (interactive) {
|
|
525
|
-
const proceed = await prompts.promptValue(
|
|
526
|
-
prompts.confirm({
|
|
527
|
-
message: `You supplied --key ${redactKey(options.key)}. Verify it matches the bearer token inside the existing OPENCODE_AUTH_JSON on ${plan.repo}. Continue?`,
|
|
528
|
-
active: 'yes',
|
|
529
|
-
inactive: 'no',
|
|
530
|
-
initialValue: false,
|
|
531
|
-
}),
|
|
532
|
-
'Setup cancelled. Run with --ack-key-reuse to bypass interactive confirmation.',
|
|
533
|
-
)
|
|
534
|
-
if (!proceed) {
|
|
535
|
-
cancelAndExit('Setup cancelled. Run with --ack-key-reuse to bypass interactive confirmation.')
|
|
536
|
-
}
|
|
537
|
-
} else if (!options.ackKeyReuse) {
|
|
538
|
-
throw new Error(
|
|
539
|
-
`Refusing key-reuse without explicit acknowledgment. Pass --ack-key-reuse to confirm that --key matches the bearer token inside the existing OPENCODE_AUTH_JSON on ${plan.repo}. (The CLI cannot verify this because GitHub secrets are write-only.)`,
|
|
540
|
-
)
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
const collisions = collectCollisions(plan.template, existingSecrets, existingVariables)
|
|
545
|
-
|
|
546
|
-
if (collisions.length > 0) {
|
|
547
|
-
if (!interactive && !options.force) {
|
|
548
|
-
throw new Error(
|
|
549
|
-
`Refusing to overwrite existing GitHub values in ${plan.repo}: ${collisions.join(', ')}. Pass --force to confirm. Note: --force only authorizes overwriting these GitHub secret values; it does NOT rotate the underlying CLIProxyAPI proxy bearer token (which is preserved byte-for-byte when --key is supplied).`,
|
|
550
|
-
)
|
|
551
|
-
}
|
|
552
|
-
|
|
553
|
-
if (!interactive && options.force) {
|
|
554
|
-
log.warn(
|
|
555
|
-
`Overwriting existing GitHub values: ${collisions.join(', ')}. ` +
|
|
556
|
-
`Concurrent setup runs against the same repo are not coordinated and resolve last-write-wins — don't run setup against this repo from two places at once.`,
|
|
557
|
-
)
|
|
558
|
-
// proceed
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
if (interactive) {
|
|
562
|
-
log.warn(`Existing GitHub values will be overwritten: ${collisions.join(', ')}`)
|
|
563
|
-
const overwrite = await prompts.promptValue(
|
|
564
|
-
prompts.confirm({
|
|
565
|
-
message: 'Overwrite the existing GitHub values?',
|
|
566
|
-
active: 'overwrite',
|
|
567
|
-
inactive: 'cancel',
|
|
568
|
-
initialValue: false,
|
|
569
|
-
}),
|
|
570
|
-
'Setup cancelled instead of overwriting existing values.',
|
|
571
|
-
)
|
|
572
|
-
|
|
573
|
-
if (!overwrite) {
|
|
574
|
-
cancelAndExit('Existing GitHub values left unchanged.')
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
let keyCreatedByThisRun = false
|
|
580
|
-
const managementKey = plan.createKey ? mgmtKeyResolver() : undefined
|
|
581
|
-
|
|
582
|
-
if (plan.createKey && managementKey) {
|
|
583
|
-
await withSpinner('Creating a new CLIProxyAPI key', async () => {
|
|
584
|
-
await gh.createManagementApiKey(baseUrl, managementKey, plan.keyValue)
|
|
585
|
-
keyCreatedByThisRun = true
|
|
586
|
-
})
|
|
587
|
-
}
|
|
588
|
-
|
|
589
|
-
try {
|
|
590
|
-
await gh.withGhRetry(
|
|
591
|
-
'Writing GitHub secrets and variables',
|
|
592
|
-
async spinnerInstance => {
|
|
593
|
-
for (const secret of plan.template.secrets) {
|
|
594
|
-
spinnerInstance.message(`Setting secret ${secret.name}`)
|
|
595
|
-
await gh.applyGhValue('secret', secret.name, plan.repo, secret.value)
|
|
596
|
-
}
|
|
597
|
-
|
|
598
|
-
for (const variable of plan.template.variables) {
|
|
599
|
-
spinnerInstance.message(`Setting variable ${variable.name}`)
|
|
600
|
-
await gh.applyGhValue('variable', variable.name, plan.repo, variable.value)
|
|
601
|
-
}
|
|
602
|
-
},
|
|
603
|
-
interactive,
|
|
604
|
-
)
|
|
605
|
-
|
|
606
|
-
await verifyWrittenNamesVisible(
|
|
607
|
-
plan.repo,
|
|
608
|
-
plan.template.secrets.map(s => s.name),
|
|
609
|
-
plan.template.variables.map(v => v.name),
|
|
610
|
-
gh.listExistingGhNames,
|
|
611
|
-
)
|
|
612
|
-
|
|
613
|
-
await withSpinner('Verifying the new key through the proxy', async () => {
|
|
614
|
-
await validation.assertProxyKeyWorks(baseUrl, plan.keyValue)
|
|
615
|
-
})
|
|
616
|
-
|
|
617
|
-
if (plan.harness === 'opencode') {
|
|
618
|
-
const workflow = await gh.withGhRetry(
|
|
619
|
-
`Checking ${plan.repo} fro-bot.yaml wiring`,
|
|
620
|
-
async () => {
|
|
621
|
-
return checkFroBotWorkflow(plan.repo)
|
|
622
|
-
},
|
|
623
|
-
interactive,
|
|
624
|
-
)
|
|
625
|
-
|
|
626
|
-
switch (workflow.kind) {
|
|
627
|
-
case 'missing': {
|
|
628
|
-
log.warn(
|
|
629
|
-
`No .github/workflows/fro-bot.yaml found in ${plan.repo}. The secrets and variables are set, but Fro Bot won't run until the workflow exists and passes them as inputs. See marcusrbrown/infra/.github/workflows/fro-bot.yaml for a reference.`,
|
|
630
|
-
)
|
|
631
|
-
break
|
|
632
|
-
}
|
|
633
|
-
case 'unreachable': {
|
|
634
|
-
log.warn(
|
|
635
|
-
`Could not check .github/workflows/fro-bot.yaml in ${plan.repo}: ${workflow.reason}. The secrets and variables are set, but the workflow wiring was not verified. Re-run 'infra cliproxy setup' later to confirm, or inspect the file directly.`,
|
|
636
|
-
)
|
|
637
|
-
break
|
|
638
|
-
}
|
|
639
|
-
case 'no-agent-step': {
|
|
640
|
-
log.warn(
|
|
641
|
-
`${plan.repo} .github/workflows/fro-bot.yaml exists but has no 'fro-bot/agent' step. Add one that passes the secrets and variables just written. See marcusrbrown/infra/.github/workflows/fro-bot.yaml for a reference.`,
|
|
642
|
-
)
|
|
643
|
-
break
|
|
644
|
-
}
|
|
645
|
-
case 'analyzed': {
|
|
646
|
-
for (const step of workflow.stepsWithGaps) {
|
|
647
|
-
const missing = [...step.missingInputs]
|
|
648
|
-
log.warn(
|
|
649
|
-
[
|
|
650
|
-
`${plan.repo} .github/workflows/fro-bot.yaml fro-bot/agent step #${step.stepOrdinal} is missing ${missing.length} required input${
|
|
651
|
-
missing.length > 1 ? 's' : ''
|
|
652
|
-
} (${missing.join(', ')}).`,
|
|
653
|
-
`Without ${missing.includes('opencode-config') ? 'opencode-config, the baseURL override is ignored and Fro Bot hits api.anthropic.com with the proxy key, which fails with 401' : 'these, the secrets you just wrote will not reach OpenCode'}.`,
|
|
654
|
-
'',
|
|
655
|
-
`Add under the 'with:' block of the 'fro-bot/agent' step:`,
|
|
656
|
-
formatWorkflowSnippet(missing),
|
|
657
|
-
].join('\n'),
|
|
658
|
-
)
|
|
659
|
-
}
|
|
660
|
-
break
|
|
661
|
-
}
|
|
662
|
-
default: {
|
|
663
|
-
const _exhaustive: never = workflow
|
|
664
|
-
throw new Error(`Unhandled FroBotWorkflowCheck kind: ${JSON.stringify(_exhaustive)}`)
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
}
|
|
668
|
-
} catch (mutationError) {
|
|
669
|
-
if (keyCreatedByThisRun && managementKey) {
|
|
670
|
-
try {
|
|
671
|
-
await gh.deleteManagementApiKey(baseUrl, managementKey, plan.keyValue)
|
|
672
|
-
log.warn('Rolled back the newly created CLIProxyAPI key after failure.')
|
|
673
|
-
} catch {
|
|
674
|
-
log.warn(
|
|
675
|
-
'Failed to roll back the newly created CLIProxyAPI key. Remove it manually via: infra cliproxy keys remove',
|
|
676
|
-
)
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
throw mutationError
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
if (interactive) {
|
|
683
|
-
prompts.outro(`Setup complete for ${plan.repo}. The ${plan.harness} harness can now use ${baseUrl}/v1.`)
|
|
684
|
-
} else {
|
|
685
|
-
log.success(`Setup complete for ${plan.repo}.`)
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
// ── Smoke test (opt-in, non-blocking) ──────────────────────────────
|
|
689
|
-
if (options.verifySmoke) {
|
|
690
|
-
const smokeResult = await withSpinner('Running smoke test', async () =>
|
|
691
|
-
smoke.runSmokeTest(plan.repo, plan.template.variables.find(v => v.name === 'FRO_BOT_MODEL')?.value ?? ''),
|
|
692
|
-
).catch(async error => {
|
|
693
|
-
// withSpinner re-throws; catch here so smoke test never gates setup
|
|
694
|
-
return {
|
|
695
|
-
kind: 'unverified' as const,
|
|
696
|
-
message: `Smoke test error: ${extractErrorMessage(error)}`,
|
|
697
|
-
runUrl: undefined,
|
|
698
|
-
}
|
|
699
|
-
})
|
|
700
|
-
|
|
701
|
-
// Machine-parseable hook for MCP/agent consumers
|
|
702
|
-
ctx.console.log(`[smoke-test] kind=${smokeResult.kind}`)
|
|
703
|
-
|
|
704
|
-
switch (smokeResult.kind) {
|
|
705
|
-
case 'pass': {
|
|
706
|
-
log.success(`✓ ${smokeResult.message}${smokeResult.runUrl ? ` — ${smokeResult.runUrl}` : ''}`)
|
|
707
|
-
break
|
|
708
|
-
}
|
|
709
|
-
case 'fail': {
|
|
710
|
-
log.warn(`✗ ${smokeResult.message}${smokeResult.runUrl ? ` — ${smokeResult.runUrl}` : ''}`)
|
|
711
|
-
break
|
|
712
|
-
}
|
|
713
|
-
case 'unverified': {
|
|
714
|
-
log.warn(`⚠ ${smokeResult.message}${smokeResult.runUrl ? ` — ${smokeResult.runUrl}` : ''}`)
|
|
715
|
-
break
|
|
716
|
-
}
|
|
717
|
-
default: {
|
|
718
|
-
const _exhaustive: never = smokeResult
|
|
719
|
-
throw new Error(`Unhandled SmokeResult kind: ${JSON.stringify(_exhaustive)}`)
|
|
720
|
-
}
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
} catch (error) {
|
|
724
|
-
const message = extractErrorMessage(error)
|
|
725
|
-
// MCP-mounted callers need ctx.console.error to surface the message through the MCP transport.
|
|
726
|
-
// Bare CLI callers leave it to cli.ts's top-level catch — emitting here too would double-log.
|
|
727
|
-
if (ctxInjected) {
|
|
728
|
-
ctx.console.error(message)
|
|
729
|
-
}
|
|
730
|
-
if (interactive) {
|
|
731
|
-
cancel(message)
|
|
732
|
-
}
|
|
733
|
-
throw error
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
export function registerCliproxySetup(cli: ReturnType<typeof goke>): void {
|
|
738
|
-
cli
|
|
739
|
-
.command(
|
|
740
|
-
'cliproxy setup',
|
|
741
|
-
'Interactively onboard a GitHub repository to CLIProxyAPI by creating or reusing a key and wiring the required GitHub secrets and variables.',
|
|
742
|
-
)
|
|
743
|
-
.option(
|
|
744
|
-
'--key [key]',
|
|
745
|
-
z
|
|
746
|
-
.string()
|
|
747
|
-
.describe(
|
|
748
|
-
'Existing CLIProxyAPI API key value. When provided, setup skips key creation and reuses this key for GitHub secrets.',
|
|
749
|
-
),
|
|
750
|
-
)
|
|
751
|
-
.option(
|
|
752
|
-
'--repo [repo]',
|
|
753
|
-
z.string().describe('Target GitHub repository in owner/repo format. Skips the repository prompt when provided.'),
|
|
754
|
-
)
|
|
755
|
-
.option(
|
|
756
|
-
'--harness [harness]',
|
|
757
|
-
harnessSchema.describe(
|
|
758
|
-
'Harness template to configure. Choose opencode, claude-code, or generic. Generic remains interactive-only.',
|
|
759
|
-
),
|
|
760
|
-
)
|
|
761
|
-
.option(
|
|
762
|
-
'--providers [providers]',
|
|
763
|
-
z
|
|
764
|
-
.string()
|
|
765
|
-
.describe(
|
|
766
|
-
'Comma-separated list of providers to enable. Default: anthropic. Supported values: anthropic, openai. Example: --providers anthropic,openai',
|
|
767
|
-
),
|
|
768
|
-
)
|
|
769
|
-
.option(
|
|
770
|
-
'--model [model]',
|
|
771
|
-
z
|
|
772
|
-
.string()
|
|
773
|
-
.regex(MODEL_ID_RE)
|
|
774
|
-
.describe(
|
|
775
|
-
'Override the default model. Must be provider-prefixed and lowercase. Required when multiple providers selected. Examples: anthropic/claude-sonnet-4-6, openai/gpt-4o',
|
|
776
|
-
),
|
|
777
|
-
)
|
|
778
|
-
.option(
|
|
779
|
-
'--force',
|
|
780
|
-
z.boolean().optional().describe('Overwrite existing GitHub secrets and variables without prompting.'),
|
|
781
|
-
)
|
|
782
|
-
.option('--dry-run', z.boolean().optional().describe('Print the plan without applying any changes.'))
|
|
783
|
-
.option(
|
|
784
|
-
'--verify-smoke',
|
|
785
|
-
z.boolean().optional().describe('Run a smoke test against the proxy after setup completes.'),
|
|
786
|
-
)
|
|
787
|
-
.option(
|
|
788
|
-
'--ack-key-reuse',
|
|
789
|
-
z
|
|
790
|
-
.boolean()
|
|
791
|
-
.default(false)
|
|
792
|
-
.describe(
|
|
793
|
-
'Acknowledge that --key matches the bearer token inside the existing OPENCODE_AUTH_JSON. Required in non-interactive mode when --key is supplied for a repo with existing OPENCODE_AUTH_JSON.',
|
|
794
|
-
),
|
|
795
|
-
)
|
|
796
|
-
.example('# Preview planned actions without applying any changes (no flags required)')
|
|
797
|
-
.example('infra cliproxy setup --dry-run')
|
|
798
|
-
.example('# Run the interactive onboarding wizard')
|
|
799
|
-
.example('infra cliproxy setup')
|
|
800
|
-
.example('# Run non-interactively with an existing key (anthropic-only)')
|
|
801
|
-
.example('infra cliproxy setup --repo owner/repo --harness opencode --key sk-existing --force')
|
|
802
|
-
.example('# Enable both providers non-interactively (requires --force and --model)')
|
|
803
|
-
.example(
|
|
804
|
-
'infra cliproxy setup --repo owner/repo --harness opencode --providers anthropic,openai --model openai/gpt-5.4-mini --key sk-existing --ack-key-reuse --force',
|
|
805
|
-
)
|
|
806
|
-
.action(async (options, ctx) => {
|
|
807
|
-
await runSetupCommand(options, {ctx})
|
|
808
|
-
})
|
|
809
|
-
}
|