@marcusrbrown/infra 0.13.0 → 0.13.2

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.
Files changed (108) hide show
  1. package/dist/cli.js +4977 -0
  2. package/package.json +7 -3
  3. package/src/__snapshots__/cli.test.ts.snap +0 -219
  4. package/src/__test__/mcp-ctx-fixture.test.ts +0 -108
  5. package/src/__test__/mcp-ctx-fixture.ts +0 -104
  6. package/src/cli.test.ts +0 -66
  7. package/src/cli.ts +0 -43
  8. package/src/commands/cliproxy/config.test.ts +0 -475
  9. package/src/commands/cliproxy/config.ts +0 -238
  10. package/src/commands/cliproxy/deploy.test.ts +0 -181
  11. package/src/commands/cliproxy/deploy.ts +0 -157
  12. package/src/commands/cliproxy/host.test.ts +0 -73
  13. package/src/commands/cliproxy/host.ts +0 -31
  14. package/src/commands/cliproxy/index.ts +0 -21
  15. package/src/commands/cliproxy/keys.test.ts +0 -326
  16. package/src/commands/cliproxy/keys.ts +0 -201
  17. package/src/commands/cliproxy/login.test.ts +0 -415
  18. package/src/commands/cliproxy/login.ts +0 -131
  19. package/src/commands/cliproxy/models.test.ts +0 -592
  20. package/src/commands/cliproxy/models.ts +0 -237
  21. package/src/commands/cliproxy/open.test.ts +0 -132
  22. package/src/commands/cliproxy/open.ts +0 -78
  23. package/src/commands/cliproxy/setup/gh.test.ts +0 -218
  24. package/src/commands/cliproxy/setup/gh.ts +0 -250
  25. package/src/commands/cliproxy/setup/preview.test.ts +0 -159
  26. package/src/commands/cliproxy/setup/preview.ts +0 -41
  27. package/src/commands/cliproxy/setup/prompts.test.ts +0 -58
  28. package/src/commands/cliproxy/setup/prompts.ts +0 -99
  29. package/src/commands/cliproxy/setup/providers.test.ts +0 -245
  30. package/src/commands/cliproxy/setup/providers.ts +0 -136
  31. package/src/commands/cliproxy/setup/smoke-test.test.ts +0 -821
  32. package/src/commands/cliproxy/setup/smoke-test.ts +0 -223
  33. package/src/commands/cliproxy/setup/templates.test.ts +0 -357
  34. package/src/commands/cliproxy/setup/templates.ts +0 -145
  35. package/src/commands/cliproxy/setup/validation.test.ts +0 -567
  36. package/src/commands/cliproxy/setup/validation.ts +0 -209
  37. package/src/commands/cliproxy/setup/workflow-analyzer.test.ts +0 -363
  38. package/src/commands/cliproxy/setup/workflow-analyzer.ts +0 -136
  39. package/src/commands/cliproxy/setup.test.ts +0 -2366
  40. package/src/commands/cliproxy/setup.ts +0 -809
  41. package/src/commands/cliproxy/shared.test.ts +0 -118
  42. package/src/commands/cliproxy/shared.ts +0 -84
  43. package/src/commands/cliproxy/status.test.ts +0 -1157
  44. package/src/commands/cliproxy/status.ts +0 -597
  45. package/src/commands/dashboard/deploy.test.ts +0 -215
  46. package/src/commands/dashboard/deploy.ts +0 -135
  47. package/src/commands/dashboard/host.test.ts +0 -79
  48. package/src/commands/dashboard/host.ts +0 -33
  49. package/src/commands/dashboard/index.ts +0 -13
  50. package/src/commands/dashboard/logs.test.ts +0 -317
  51. package/src/commands/dashboard/logs.ts +0 -180
  52. package/src/commands/dashboard/status.test.ts +0 -708
  53. package/src/commands/dashboard/status.ts +0 -293
  54. package/src/commands/gateway/backup.test.ts +0 -426
  55. package/src/commands/gateway/backup.ts +0 -205
  56. package/src/commands/gateway/deploy.test.ts +0 -491
  57. package/src/commands/gateway/deploy.ts +0 -173
  58. package/src/commands/gateway/host.test.ts +0 -89
  59. package/src/commands/gateway/host.ts +0 -33
  60. package/src/commands/gateway/index.ts +0 -17
  61. package/src/commands/gateway/logs.test.ts +0 -263
  62. package/src/commands/gateway/logs.ts +0 -160
  63. package/src/commands/gateway/restore.test.ts +0 -650
  64. package/src/commands/gateway/restore.ts +0 -308
  65. package/src/commands/gateway/status.test.ts +0 -755
  66. package/src/commands/gateway/status.ts +0 -255
  67. package/src/commands/keeweb/deploy.test.ts +0 -177
  68. package/src/commands/keeweb/deploy.ts +0 -191
  69. package/src/commands/keeweb/index.ts +0 -11
  70. package/src/commands/keeweb/open.test.ts +0 -54
  71. package/src/commands/keeweb/open.ts +0 -30
  72. package/src/commands/keeweb/status.test.ts +0 -509
  73. package/src/commands/keeweb/status.ts +0 -359
  74. package/src/commands/mcp.test.ts +0 -260
  75. package/src/commands/mcp.ts +0 -58
  76. package/src/commands/status.test.ts +0 -222
  77. package/src/commands/status.ts +0 -161
  78. package/src/commands/umami/deploy.test.ts +0 -202
  79. package/src/commands/umami/deploy.ts +0 -132
  80. package/src/commands/umami/host.test.ts +0 -79
  81. package/src/commands/umami/host.ts +0 -33
  82. package/src/commands/umami/index.ts +0 -13
  83. package/src/commands/umami/logs.test.ts +0 -191
  84. package/src/commands/umami/logs.ts +0 -163
  85. package/src/commands/umami/status.test.ts +0 -633
  86. package/src/commands/umami/status.ts +0 -285
  87. package/src/commands/vpn/client.test.ts +0 -949
  88. package/src/commands/vpn/client.ts +0 -467
  89. package/src/commands/vpn/deploy.test.ts +0 -208
  90. package/src/commands/vpn/deploy.ts +0 -143
  91. package/src/commands/vpn/host.test.ts +0 -89
  92. package/src/commands/vpn/host.ts +0 -36
  93. package/src/commands/vpn/index.ts +0 -15
  94. package/src/commands/vpn/logs.test.ts +0 -234
  95. package/src/commands/vpn/logs.ts +0 -136
  96. package/src/commands/vpn/peers.test.ts +0 -478
  97. package/src/commands/vpn/shared.test.ts +0 -454
  98. package/src/commands/vpn/shared.ts +0 -170
  99. package/src/commands/vpn/status.test.ts +0 -142
  100. package/src/commands/vpn/status.ts +0 -78
  101. package/src/conventions.test.ts +0 -1180
  102. package/src/lib/action-ctx.ts +0 -25
  103. package/src/lib/known-hosts.test.ts +0 -192
  104. package/src/lib/known-hosts.ts +0 -76
  105. package/src/lib/redact.ts +0 -21
  106. package/src/lib/ssh-identity.test.ts +0 -134
  107. package/src/lib/ssh-identity.ts +0 -76
  108. /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
- }