@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.
Files changed (108) hide show
  1. package/dist/cli.js +11965 -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,491 +0,0 @@
1
- import {resolve} from 'node:path'
2
- import {afterEach, beforeEach, describe, expect, it, spyOn} from 'bun:test'
3
-
4
- import {getGatewayDeployEnv, validateGatewayRemotePreconditions} from './deploy'
5
-
6
- const cliDir = resolve(import.meta.dir, '../../..')
7
-
8
- const envKeys = [
9
- 'AWS_ACCESS_KEY_ID',
10
- 'AWS_SECRET_ACCESS_KEY',
11
- 'AWS_SESSION_TOKEN',
12
- 'DISCORD_APPLICATION_ID',
13
- 'DISCORD_GUILD_ID',
14
- 'DISCORD_TOKEN',
15
- 'GATEWAY_HOST',
16
- 'GATEWAY_PRESENCE_CHANNEL_ID',
17
- 'GATEWAY_TRIGGER_ROLE_ID',
18
- 'GATEWAY_WEBHOOK_SECRET',
19
- 'HOME',
20
- 'OBJECT_STORE_HOSTS',
21
- 'PATH',
22
- 'S3_BUCKET',
23
- 'S3_ENDPOINT',
24
- 'S3_REGION',
25
- 'SSH_AUTH_SOCK',
26
- 'WORKSPACE_OPENCODE_AUTH',
27
- 'WORKSPACE_OPENCODE_CONFIG',
28
- 'WORKSPACE_OPENCODE_MODEL',
29
- 'WORKSPACE_OPENCODE_TOKEN',
30
- 'WORKSPACE_OPENCODE_URL',
31
- ] as const
32
-
33
- type ManagedEnvKey = (typeof envKeys)[number]
34
-
35
- let originalEnv: Partial<Record<ManagedEnvKey, string | undefined>>
36
-
37
- function restoreManagedEnv(): void {
38
- for (const key of envKeys) {
39
- const value = originalEnv[key]
40
-
41
- if (value === undefined) {
42
- delete process.env[key]
43
- continue
44
- }
45
-
46
- process.env[key] = value
47
- }
48
- }
49
-
50
- function setManagedEnv(overrides: Partial<Record<ManagedEnvKey, string | undefined>>): void {
51
- restoreManagedEnv()
52
-
53
- for (const [key, value] of Object.entries(overrides)) {
54
- if (value === undefined) {
55
- delete process.env[key]
56
- continue
57
- }
58
-
59
- process.env[key as ManagedEnvKey] = value
60
- }
61
- }
62
-
63
- async function runDeployCommand(
64
- args: string[],
65
- envOverrides: Partial<Record<ManagedEnvKey, string | undefined>> = {},
66
- ): Promise<{stdout: string; stderr: string; exitCode: number}> {
67
- const env: Record<string, string> = {}
68
-
69
- for (const [key, value] of Object.entries(process.env)) {
70
- if (value !== undefined) {
71
- env[key] = value
72
- }
73
- }
74
-
75
- // Apply overrides: undefined means explicitly unset the key
76
- for (const [key, value] of Object.entries(envOverrides)) {
77
- if (value === undefined) {
78
- delete env[key]
79
- } else {
80
- env[key] = value
81
- }
82
- }
83
-
84
- // Apply defaults only for keys not explicitly overridden
85
- if (!Object.prototype.hasOwnProperty.call(envOverrides, 'HOME') && !env.HOME) {
86
- env.HOME = '/tmp/test-home'
87
- }
88
-
89
- if (!Object.prototype.hasOwnProperty.call(envOverrides, 'PATH') && !env.PATH) {
90
- env.PATH = '/usr/bin:/bin'
91
- }
92
-
93
- if (!Object.prototype.hasOwnProperty.call(envOverrides, 'SSH_AUTH_SOCK') && !env.SSH_AUTH_SOCK) {
94
- env.SSH_AUTH_SOCK = '/tmp/test-sock'
95
- }
96
-
97
- env.NO_COLOR = '1'
98
-
99
- const proc = Bun.spawn(['bun', 'src/cli.ts', 'gateway', 'deploy', ...args], {
100
- cwd: cliDir,
101
- env,
102
- stdout: 'pipe',
103
- stderr: 'pipe',
104
- })
105
-
106
- const [stdout, stderr, exitCode] = await Promise.all([
107
- new Response(proc.stdout).text(),
108
- new Response(proc.stderr).text(),
109
- proc.exited,
110
- ])
111
-
112
- return {stdout, stderr, exitCode}
113
- }
114
-
115
- describe('gateway deploy', () => {
116
- beforeEach(() => {
117
- originalEnv = Object.fromEntries(envKeys.map(key => [key, process.env[key]]))
118
- })
119
-
120
- afterEach(() => {
121
- restoreManagedEnv()
122
- })
123
-
124
- describe('getGatewayDeployEnv', () => {
125
- it('returns the expected deploy environment when required variables are present', () => {
126
- setManagedEnv({
127
- GATEWAY_HOST: 'gateway.example.com',
128
- HOME: '/tmp/test-home',
129
- PATH: '/usr/bin:/bin',
130
- SSH_AUTH_SOCK: '/tmp/test-sock',
131
- })
132
-
133
- const env = getGatewayDeployEnv()
134
-
135
- expect(env.GATEWAY_HOST).toBe('gateway.example.com')
136
- expect(env.HOME).toBe('/tmp/test-home')
137
- expect(env.PATH).toBe('/usr/bin:/bin')
138
- expect(env.SSH_AUTH_SOCK).toBe('/tmp/test-sock')
139
- })
140
-
141
- it('throws when SSH_AUTH_SOCK is missing', () => {
142
- setManagedEnv({
143
- GATEWAY_HOST: 'gateway.example.com',
144
- HOME: '/tmp/test-home',
145
- PATH: '/usr/bin:/bin',
146
- SSH_AUTH_SOCK: undefined,
147
- })
148
-
149
- expect(() => getGatewayDeployEnv()).toThrow('SSH_AUTH_SOCK')
150
- })
151
-
152
- it('throws when PATH is missing', () => {
153
- setManagedEnv({
154
- GATEWAY_HOST: 'gateway.example.com',
155
- HOME: '/tmp/test-home',
156
- PATH: undefined,
157
- SSH_AUTH_SOCK: '/tmp/test-sock',
158
- })
159
-
160
- expect(() => getGatewayDeployEnv()).toThrow('PATH')
161
- })
162
-
163
- it('passes required gateway env vars to the child process', () => {
164
- setManagedEnv({
165
- AWS_ACCESS_KEY_ID: 'test-key-id',
166
- AWS_SECRET_ACCESS_KEY: 'test-secret',
167
- AWS_SESSION_TOKEN: undefined,
168
- DISCORD_APPLICATION_ID: 'test-app-id',
169
- DISCORD_GUILD_ID: 'test-guild-id',
170
- DISCORD_TOKEN: 'test-token',
171
- GATEWAY_HOST: 'gateway.example.com',
172
- HOME: '/tmp/test-home',
173
- OBJECT_STORE_HOSTS: undefined,
174
- PATH: '/usr/bin:/bin',
175
- S3_BUCKET: 'test-bucket',
176
- S3_ENDPOINT: undefined,
177
- S3_REGION: 'us-east-1',
178
- SSH_AUTH_SOCK: '/tmp/test-sock',
179
- })
180
-
181
- const env = getGatewayDeployEnv()
182
-
183
- expect(env.DISCORD_TOKEN).toBe('test-token')
184
- expect(env.AWS_ACCESS_KEY_ID).toBe('test-key-id')
185
- expect(env.AWS_SECRET_ACCESS_KEY).toBe('test-secret')
186
- expect(env.DISCORD_APPLICATION_ID).toBe('test-app-id')
187
- expect(env.DISCORD_GUILD_ID).toBe('test-guild-id')
188
- expect(env.S3_BUCKET).toBe('test-bucket')
189
- expect(env.S3_REGION).toBe('us-east-1')
190
- expect(env.GATEWAY_HOST).toBe('gateway.example.com')
191
- // Optional vars present (empty string when unset)
192
- expect(env.S3_ENDPOINT).toBe('')
193
- expect(env.OBJECT_STORE_HOSTS).toBe('')
194
- expect(env.AWS_SESSION_TOKEN).toBe('')
195
- // Core vars still present
196
- expect(env.PATH).toBe('/usr/bin:/bin')
197
- expect(env.HOME).toBe('/tmp/test-home')
198
- expect(env.SSH_AUTH_SOCK).toBe('/tmp/test-sock')
199
- })
200
-
201
- it('forwards S3_ENDPOINT and OBJECT_STORE_HOSTS when present in process.env', () => {
202
- setManagedEnv({
203
- GATEWAY_HOST: 'gateway.example.com',
204
- HOME: '/tmp/test-home',
205
- OBJECT_STORE_HOSTS: 'r2.example.com minio.example.com',
206
- PATH: '/usr/bin:/bin',
207
- S3_ENDPOINT: 'https://r2.example.com',
208
- SSH_AUTH_SOCK: '/tmp/test-sock',
209
- })
210
-
211
- const env = getGatewayDeployEnv()
212
-
213
- expect(env.S3_ENDPOINT).toBe('https://r2.example.com')
214
- expect(env.OBJECT_STORE_HOSTS).toBe('r2.example.com minio.example.com')
215
- })
216
-
217
- it('forwards AWS_SESSION_TOKEN when present in process.env', () => {
218
- setManagedEnv({
219
- AWS_SESSION_TOKEN: 'sts-temporary-token-value',
220
- GATEWAY_HOST: 'gateway.example.com',
221
- HOME: '/tmp/test-home',
222
- PATH: '/usr/bin:/bin',
223
- SSH_AUTH_SOCK: '/tmp/test-sock',
224
- })
225
-
226
- const env = getGatewayDeployEnv()
227
-
228
- expect(env.AWS_SESSION_TOKEN).toBe('sts-temporary-token-value')
229
- })
230
-
231
- it('forwards GH_APP_ID and GH_APP_PRIVATE_KEY when present in process.env', () => {
232
- setManagedEnv({
233
- GATEWAY_HOST: 'gateway.example.com',
234
- HOME: '/tmp/test-home',
235
- PATH: '/usr/bin:/bin',
236
- SSH_AUTH_SOCK: '/tmp/test-sock',
237
- })
238
- process.env.GH_APP_ID = '999888'
239
- process.env.GH_APP_PRIVATE_KEY = '-----BEGIN RSA PRIVATE KEY-----\nfakekey\n-----END RSA PRIVATE KEY-----\n'
240
-
241
- try {
242
- const env = getGatewayDeployEnv()
243
- expect(env.GH_APP_ID).toBe('999888')
244
- expect(env.GH_APP_PRIVATE_KEY).toBe('-----BEGIN RSA PRIVATE KEY-----\nfakekey\n-----END RSA PRIVATE KEY-----\n')
245
- } finally {
246
- delete process.env.GH_APP_ID
247
- delete process.env.GH_APP_PRIVATE_KEY
248
- }
249
- })
250
-
251
- it('forwards DISCORD_PRIVILEGED_INTENTS when present in process.env', () => {
252
- setManagedEnv({
253
- GATEWAY_HOST: 'gateway.example.com',
254
- HOME: '/tmp/test-home',
255
- PATH: '/usr/bin:/bin',
256
- SSH_AUTH_SOCK: '/tmp/test-sock',
257
- })
258
- process.env.DISCORD_PRIVILEGED_INTENTS = 'GUILD_MEMBERS'
259
-
260
- try {
261
- const env = getGatewayDeployEnv()
262
- expect(env.DISCORD_PRIVILEGED_INTENTS).toBe('GUILD_MEMBERS')
263
- } finally {
264
- delete process.env.DISCORD_PRIVILEGED_INTENTS
265
- }
266
- })
267
-
268
- it('DISCORD_PRIVILEGED_INTENTS unset → forwarded as empty string', () => {
269
- setManagedEnv({
270
- GATEWAY_HOST: 'gateway.example.com',
271
- HOME: '/tmp/test-home',
272
- PATH: '/usr/bin:/bin',
273
- SSH_AUTH_SOCK: '/tmp/test-sock',
274
- })
275
- delete process.env.DISCORD_PRIVILEGED_INTENTS
276
-
277
- const env = getGatewayDeployEnv()
278
- expect(env.DISCORD_PRIVILEGED_INTENTS).toBe('')
279
- })
280
-
281
- // ── workspace mention-loop var parity ─────────────────────────────────────
282
-
283
- it('forwards WORKSPACE_OPENCODE_TOKEN when set', () => {
284
- setManagedEnv({
285
- GATEWAY_HOST: 'gateway.example.com',
286
- HOME: '/tmp/test-home',
287
- PATH: '/usr/bin:/bin',
288
- SSH_AUTH_SOCK: '/tmp/test-sock',
289
- WORKSPACE_OPENCODE_TOKEN: 'ws-tok-123',
290
- })
291
- const env = getGatewayDeployEnv()
292
- expect(env.WORKSPACE_OPENCODE_TOKEN).toBe('ws-tok-123')
293
- })
294
-
295
- it('forwards WORKSPACE_OPENCODE_AUTH when set', () => {
296
- setManagedEnv({
297
- GATEWAY_HOST: 'gateway.example.com',
298
- HOME: '/tmp/test-home',
299
- PATH: '/usr/bin:/bin',
300
- SSH_AUTH_SOCK: '/tmp/test-sock',
301
- WORKSPACE_OPENCODE_AUTH: '{"provider":{}}',
302
- })
303
- const env = getGatewayDeployEnv()
304
- expect(env.WORKSPACE_OPENCODE_AUTH).toBe('{"provider":{}}')
305
- })
306
-
307
- it('forwards WORKSPACE_OPENCODE_URL when set', () => {
308
- setManagedEnv({
309
- GATEWAY_HOST: 'gateway.example.com',
310
- HOME: '/tmp/test-home',
311
- PATH: '/usr/bin:/bin',
312
- SSH_AUTH_SOCK: '/tmp/test-sock',
313
- WORKSPACE_OPENCODE_URL: 'http://workspace:9200',
314
- })
315
- const env = getGatewayDeployEnv()
316
- expect(env.WORKSPACE_OPENCODE_URL).toBe('http://workspace:9200')
317
- })
318
-
319
- it('WORKSPACE_OPENCODE_URL unset → forwarded as empty string (optional)', () => {
320
- setManagedEnv({
321
- GATEWAY_HOST: 'gateway.example.com',
322
- HOME: '/tmp/test-home',
323
- PATH: '/usr/bin:/bin',
324
- SSH_AUTH_SOCK: '/tmp/test-sock',
325
- WORKSPACE_OPENCODE_URL: undefined,
326
- })
327
- const env = getGatewayDeployEnv()
328
- expect(env.WORKSPACE_OPENCODE_URL).toBe('')
329
- })
330
-
331
- it('forwards WORKSPACE_OPENCODE_MODEL when set', () => {
332
- setManagedEnv({
333
- GATEWAY_HOST: 'gateway.example.com',
334
- HOME: '/tmp/test-home',
335
- PATH: '/usr/bin:/bin',
336
- SSH_AUTH_SOCK: '/tmp/test-sock',
337
- WORKSPACE_OPENCODE_MODEL: 'anthropic/claude-sonnet-4-6',
338
- })
339
- const env = getGatewayDeployEnv()
340
- expect(env.WORKSPACE_OPENCODE_MODEL).toBe('anthropic/claude-sonnet-4-6')
341
- })
342
-
343
- it('forwards WORKSPACE_OPENCODE_CONFIG when set', () => {
344
- setManagedEnv({
345
- GATEWAY_HOST: 'gateway.example.com',
346
- HOME: '/tmp/test-home',
347
- PATH: '/usr/bin:/bin',
348
- SSH_AUTH_SOCK: '/tmp/test-sock',
349
- WORKSPACE_OPENCODE_CONFIG: '{"provider":{}}',
350
- })
351
- const env = getGatewayDeployEnv()
352
- expect(env.WORKSPACE_OPENCODE_CONFIG).toBe('{"provider":{}}')
353
- })
354
-
355
- it('forwards GATEWAY_TRIGGER_ROLE_ID when set', () => {
356
- setManagedEnv({
357
- GATEWAY_HOST: 'gateway.example.com',
358
- HOME: '/tmp/test-home',
359
- PATH: '/usr/bin:/bin',
360
- SSH_AUTH_SOCK: '/tmp/test-sock',
361
- GATEWAY_TRIGGER_ROLE_ID: '987654321098765432',
362
- })
363
- const env = getGatewayDeployEnv()
364
- expect(env.GATEWAY_TRIGGER_ROLE_ID).toBe('987654321098765432')
365
- })
366
-
367
- // ── announce opt-in vars parity ───────────────────────────────────────────
368
-
369
- it('forwards GATEWAY_WEBHOOK_SECRET when set', () => {
370
- setManagedEnv({
371
- GATEWAY_HOST: 'gateway.example.com',
372
- GATEWAY_WEBHOOK_SECRET: 'hmac-secret-value',
373
- HOME: '/tmp/test-home',
374
- PATH: '/usr/bin:/bin',
375
- SSH_AUTH_SOCK: '/tmp/test-sock',
376
- })
377
- const env = getGatewayDeployEnv()
378
- expect(env.GATEWAY_WEBHOOK_SECRET).toBe('hmac-secret-value')
379
- })
380
-
381
- it('GATEWAY_WEBHOOK_SECRET unset → forwarded as empty string', () => {
382
- setManagedEnv({
383
- GATEWAY_HOST: 'gateway.example.com',
384
- GATEWAY_WEBHOOK_SECRET: undefined,
385
- HOME: '/tmp/test-home',
386
- PATH: '/usr/bin:/bin',
387
- SSH_AUTH_SOCK: '/tmp/test-sock',
388
- })
389
- const env = getGatewayDeployEnv()
390
- expect(env.GATEWAY_WEBHOOK_SECRET).toBe('')
391
- })
392
-
393
- it('forwards GATEWAY_PRESENCE_CHANNEL_ID when set', () => {
394
- setManagedEnv({
395
- GATEWAY_HOST: 'gateway.example.com',
396
- GATEWAY_PRESENCE_CHANNEL_ID: '111222333444555666',
397
- HOME: '/tmp/test-home',
398
- PATH: '/usr/bin:/bin',
399
- SSH_AUTH_SOCK: '/tmp/test-sock',
400
- })
401
- const env = getGatewayDeployEnv()
402
- expect(env.GATEWAY_PRESENCE_CHANNEL_ID).toBe('111222333444555666')
403
- })
404
-
405
- it('GATEWAY_PRESENCE_CHANNEL_ID unset → forwarded as empty string', () => {
406
- setManagedEnv({
407
- GATEWAY_HOST: 'gateway.example.com',
408
- GATEWAY_PRESENCE_CHANNEL_ID: undefined,
409
- HOME: '/tmp/test-home',
410
- PATH: '/usr/bin:/bin',
411
- SSH_AUTH_SOCK: '/tmp/test-sock',
412
- })
413
- const env = getGatewayDeployEnv()
414
- expect(env.GATEWAY_PRESENCE_CHANNEL_ID).toBe('')
415
- })
416
- })
417
-
418
- describe('validateGatewayRemotePreconditions', () => {
419
- it('throws when gh is not available', () => {
420
- const spy = spyOn(Bun, 'which').mockReturnValue(null)
421
-
422
- try {
423
- expect(() => validateGatewayRemotePreconditions()).toThrow('gh')
424
- } finally {
425
- spy.mockRestore()
426
- }
427
- })
428
-
429
- it('does not throw when gh is available', () => {
430
- const spy = spyOn(Bun, 'which').mockReturnValue('/usr/bin/gh')
431
-
432
- try {
433
- expect(() => validateGatewayRemotePreconditions()).not.toThrow()
434
- } finally {
435
- spy.mockRestore()
436
- }
437
- })
438
- })
439
-
440
- describe('--remote (default) dry-run', () => {
441
- it('prints planned actions without invoking gh', async () => {
442
- const {stdout, exitCode} = await runDeployCommand(['--dry-run'], {
443
- HOME: '/tmp/test-home',
444
- PATH: process.env.PATH ?? '/usr/bin:/bin',
445
- SSH_AUTH_SOCK: '/tmp/test-sock',
446
- })
447
-
448
- expect(exitCode).toBe(0)
449
- expect(stdout).toContain('Dry run')
450
- expect(stdout).toContain('Deploy Gateway')
451
- })
452
- })
453
-
454
- describe('--local --dry-run', () => {
455
- it('prints planned local actions without spawning bun run', async () => {
456
- const {stdout, exitCode} = await runDeployCommand(['--local', '--dry-run'], {
457
- HOME: '/tmp/test-home',
458
- PATH: process.env.PATH ?? '/usr/bin:/bin',
459
- SSH_AUTH_SOCK: '/tmp/test-sock',
460
- })
461
-
462
- expect(exitCode).toBe(0)
463
- expect(stdout).toContain('Dry run')
464
- expect(stdout).toContain('local')
465
- })
466
-
467
- it('succeeds even when SSH_AUTH_SOCK is unset', async () => {
468
- const {stdout, exitCode} = await runDeployCommand(['--local', '--dry-run'], {
469
- HOME: '/tmp/test-home',
470
- PATH: process.env.PATH ?? '/usr/bin:/bin',
471
- SSH_AUTH_SOCK: undefined,
472
- })
473
-
474
- expect(exitCode).toBe(0)
475
- expect(stdout).toContain('Dry run')
476
- })
477
- })
478
-
479
- describe('--local --force-recreate --dry-run', () => {
480
- it('includes --force-recreate in the planned command', async () => {
481
- const {stdout, exitCode} = await runDeployCommand(['--local', '--force-recreate', '--dry-run'], {
482
- HOME: '/tmp/test-home',
483
- PATH: process.env.PATH ?? '/usr/bin:/bin',
484
- SSH_AUTH_SOCK: '/tmp/test-sock',
485
- })
486
-
487
- expect(exitCode).toBe(0)
488
- expect(stdout).toContain('--force-recreate')
489
- })
490
- })
491
- })
@@ -1,173 +0,0 @@
1
- import type {goke} from 'goke'
2
-
3
- import {z} from 'zod'
4
-
5
- declare const process: {
6
- env: Record<string, string | undefined>
7
- exitCode?: number
8
- }
9
-
10
- const REPO = 'marcusrbrown/infra'
11
- const WORKFLOW_NAME = 'Deploy Gateway'
12
- const WORKFLOW_URL = 'https://github.com/marcusrbrown/infra/actions/workflows/deploy-gateway.yaml'
13
-
14
- type CliInstance = ReturnType<typeof goke>
15
-
16
- // ─── Env helpers ─────────────────────────────────────────────────────────────
17
-
18
- export function getGatewayDeployEnv(): Record<string, string> {
19
- const path = process.env.PATH
20
- const home = process.env.HOME
21
- const sshAuthSock = process.env.SSH_AUTH_SOCK
22
-
23
- if (!path) {
24
- throw new Error('PATH is required for local deploy')
25
- }
26
-
27
- if (!home) {
28
- throw new Error('HOME is required for local deploy')
29
- }
30
-
31
- if (!sshAuthSock) {
32
- throw new Error('SSH_AUTH_SOCK is required for local deploy. Start ssh-agent and load your deploy key first.')
33
- }
34
-
35
- return {
36
- PATH: path,
37
- HOME: home,
38
- SSH_AUTH_SOCK: sshAuthSock,
39
- GATEWAY_HOST: process.env.GATEWAY_HOST ?? '',
40
- DISCORD_TOKEN: process.env.DISCORD_TOKEN ?? '',
41
- DISCORD_APPLICATION_ID: process.env.DISCORD_APPLICATION_ID ?? '',
42
- DISCORD_GUILD_ID: process.env.DISCORD_GUILD_ID ?? '',
43
- AWS_ACCESS_KEY_ID: process.env.AWS_ACCESS_KEY_ID ?? '',
44
- AWS_SECRET_ACCESS_KEY: process.env.AWS_SECRET_ACCESS_KEY ?? '',
45
- AWS_SESSION_TOKEN: process.env.AWS_SESSION_TOKEN ?? '',
46
- S3_BUCKET: process.env.S3_BUCKET ?? '',
47
- S3_REGION: process.env.S3_REGION ?? '',
48
- S3_ENDPOINT: process.env.S3_ENDPOINT ?? '',
49
- OBJECT_STORE_HOSTS: process.env.OBJECT_STORE_HOSTS ?? '',
50
- GH_APP_ID: process.env.GH_APP_ID ?? '',
51
- GH_APP_PRIVATE_KEY: process.env.GH_APP_PRIVATE_KEY ?? '',
52
- DISCORD_PRIVILEGED_INTENTS: process.env.DISCORD_PRIVILEGED_INTENTS ?? '',
53
- WORKSPACE_OPENCODE_TOKEN: process.env.WORKSPACE_OPENCODE_TOKEN ?? '',
54
- WORKSPACE_OPENCODE_AUTH: process.env.WORKSPACE_OPENCODE_AUTH ?? '',
55
- WORKSPACE_OPENCODE_URL: process.env.WORKSPACE_OPENCODE_URL ?? '',
56
- WORKSPACE_OPENCODE_MODEL: process.env.WORKSPACE_OPENCODE_MODEL ?? '',
57
- WORKSPACE_OPENCODE_CONFIG: process.env.WORKSPACE_OPENCODE_CONFIG ?? '',
58
- GATEWAY_TRIGGER_ROLE_ID: process.env.GATEWAY_TRIGGER_ROLE_ID ?? '',
59
- GATEWAY_WEBHOOK_SECRET: process.env.GATEWAY_WEBHOOK_SECRET ?? '',
60
- GATEWAY_PRESENCE_CHANNEL_ID: process.env.GATEWAY_PRESENCE_CHANNEL_ID ?? '',
61
- GATEWAY_OPERATOR_BIND_HOST: process.env.GATEWAY_OPERATOR_BIND_HOST ?? '',
62
- GATEWAY_OPERATOR_BIND_PORT: process.env.GATEWAY_OPERATOR_BIND_PORT ?? '',
63
- GATEWAY_OPERATOR_PUBLIC_ORIGIN: process.env.GATEWAY_OPERATOR_PUBLIC_ORIGIN ?? '',
64
- GATEWAY_OPERATOR_GITHUB_CLIENT_ID: process.env.GATEWAY_OPERATOR_GITHUB_CLIENT_ID ?? '',
65
- GATEWAY_OPERATOR_GITHUB_CLIENT_SECRET: process.env.GATEWAY_OPERATOR_GITHUB_CLIENT_SECRET ?? '',
66
- GATEWAY_OPERATOR_CSRF_SECRET: process.env.GATEWAY_OPERATOR_CSRF_SECRET ?? '',
67
- GATEWAY_OPERATOR_ALLOWLIST: process.env.GATEWAY_OPERATOR_ALLOWLIST ?? '',
68
- GATEWAY_OPERATOR_OAUTH_ALLOWED_RETURN_PATHS: process.env.GATEWAY_OPERATOR_OAUTH_ALLOWED_RETURN_PATHS ?? '',
69
- GATEWAY_OPERATOR_OAUTH_STATE_TTL_MS: process.env.GATEWAY_OPERATOR_OAUTH_STATE_TTL_MS ?? '',
70
- GATEWAY_OPERATOR_OAUTH_MAX_OUTSTANDING_ATTEMPTS: process.env.GATEWAY_OPERATOR_OAUTH_MAX_OUTSTANDING_ATTEMPTS ?? '',
71
- GATEWAY_VPC_IP: process.env.GATEWAY_VPC_IP ?? '',
72
- DASHBOARD_VPC_IP: process.env.DASHBOARD_VPC_IP ?? '',
73
- GATEWAY_IMAGE_DIGEST: process.env.GATEWAY_IMAGE_DIGEST ?? '',
74
- WORKSPACE_IMAGE_DIGEST: process.env.WORKSPACE_IMAGE_DIGEST ?? '',
75
- }
76
- }
77
-
78
- export function validateGatewayRemotePreconditions(): void {
79
- if (!Bun.which('gh')) {
80
- throw new Error('gh CLI is required for remote deploy. Install gh and run `gh auth login`.')
81
- }
82
- }
83
-
84
- // ─── Command registration ─────────────────────────────────────────────────────
85
-
86
- export function registerGatewayDeploy(cli: CliInstance): void {
87
- cli
88
- .command(
89
- 'gateway deploy',
90
- 'Deploy the gateway. Default mode triggers the GitHub Deploy Gateway workflow, while --local runs apps/gateway deploy directly with Bun.',
91
- )
92
- .option(
93
- '--local',
94
- z
95
- .boolean()
96
- .default(false)
97
- .describe('Run local deployment with Bun using apps/gateway instead of triggering GitHub Actions.'),
98
- )
99
- .option(
100
- '--dry-run',
101
- z
102
- .boolean()
103
- .default(false)
104
- .describe(
105
- 'Validate deploy prerequisites and print planned actions without executing local deploy or dispatching workflow.',
106
- ),
107
- )
108
- .option(
109
- '--force-recreate',
110
- z.boolean().default(false).describe('Force recreate containers. Forwarded only in local mode.'),
111
- )
112
- .example('# Trigger remote GitHub Actions deploy (default mode)')
113
- .example('infra gateway deploy')
114
- .example('# Validate local deploy preconditions and planned command')
115
- .example('infra gateway deploy --local --dry-run')
116
- .example('# Run local deploy with explicit SSH agent context')
117
- .example('infra gateway deploy --local')
118
- .action(async options => {
119
- if (options.local) {
120
- const command = [
121
- 'bun',
122
- 'run',
123
- '--cwd',
124
- 'apps/gateway',
125
- 'deploy',
126
- ...(options.forceRecreate ? ['--force-recreate'] : []),
127
- ]
128
-
129
- if (options.dryRun) {
130
- console.log('Dry run: local gateway deploy')
131
- console.log(`- command: ${command.join(' ')}`)
132
- return
133
- }
134
-
135
- const env = getGatewayDeployEnv()
136
-
137
- const child = Bun.spawn(command, {
138
- env,
139
- stdout: 'inherit',
140
- stderr: 'inherit',
141
- })
142
-
143
- const exitCode = await child.exited
144
- if (exitCode !== 0) {
145
- throw new Error(`Local deploy failed with exit code ${exitCode}`)
146
- }
147
-
148
- return
149
- }
150
-
151
- validateGatewayRemotePreconditions()
152
-
153
- if (options.dryRun) {
154
- console.log('Dry run: remote gateway deploy')
155
- console.log(`- command: gh workflow run "${WORKFLOW_NAME}" --repo ${REPO}`)
156
- console.log(`- workflow URL: ${WORKFLOW_URL}`)
157
- return
158
- }
159
-
160
- const child = Bun.spawn(['gh', 'workflow', 'run', WORKFLOW_NAME, '--repo', REPO], {
161
- stdout: 'inherit',
162
- stderr: 'inherit',
163
- })
164
-
165
- const exitCode = await child.exited
166
- if (exitCode !== 0) {
167
- throw new Error(`Failed to trigger "${WORKFLOW_NAME}" workflow (exit code ${exitCode})`)
168
- }
169
-
170
- console.log(`Workflow triggered: ${WORKFLOW_URL}`)
171
- console.log('Approve the gateway environment deployment in GitHub Actions to continue.')
172
- })
173
- }