@marcusrbrown/infra 0.13.1 → 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,255 +0,0 @@
1
- import type {goke} from 'goke'
2
- import type {ActionCtx} from '../../lib/action-ctx'
3
- import type {StatusSummary} from '../status'
4
- import {z} from 'zod'
5
-
6
- import {buildKnownHostsArgs} from '../../lib/known-hosts'
7
- import {redactHost} from '../../lib/redact'
8
- import {buildIdentityArgs} from '../../lib/ssh-identity'
9
- // ─── Minimal ctx interface (subset of GokeExecutionContext used by this action) ─
10
- import {validateGatewayHost} from './host'
11
-
12
- declare const process: {
13
- env: Record<string, string | undefined>
14
- exitCode?: number
15
- }
16
-
17
- const COMPOSE_PROJECT_DIR = '/opt/gateway/deploy'
18
-
19
- // ─── Types ───────────────────────────────────────────────────────────────────
20
-
21
- export interface ComposePsEntry {
22
- Name: string
23
- State: string
24
- Health: string
25
- }
26
-
27
- export type HealthStatus = 'healthy' | 'unhealthy' | 'starting' | 'n-a'
28
-
29
- export interface ServiceRow {
30
- service: string
31
- state: string
32
- health: HealthStatus
33
- }
34
-
35
- export interface GatewayStatusResult {
36
- ok: boolean
37
- services: ServiceRow[]
38
- error?: string
39
- }
40
-
41
- export type SpawnFn = (
42
- cmd: string[],
43
- opts: {env: Record<string, string>; stdout: 'pipe'; stderr: 'pipe'},
44
- ) => {
45
- stdout: ReadableStream<Uint8Array>
46
- stderr: ReadableStream<Uint8Array>
47
- exited: Promise<number>
48
- }
49
-
50
- // ─── Pure helpers ─────────────────────────────────────────────────────────────
51
-
52
- export function parseComposePsOutput(stdoutText: string): ComposePsEntry[] {
53
- const trimmed = stdoutText.trim()
54
- if (trimmed.length === 0) return []
55
-
56
- // Legacy compose may emit a single JSON array; current versions emit NDJSON.
57
- if (trimmed.startsWith('[')) {
58
- const parsed: unknown = JSON.parse(trimmed)
59
- return Array.isArray(parsed) ? (parsed as ComposePsEntry[]) : []
60
- }
61
-
62
- // NDJSON: one JSON object per non-empty line.
63
- return trimmed
64
- .split('\n')
65
- .map(line => line.trim())
66
- .filter(line => line.length > 0)
67
- .map(line => JSON.parse(line) as ComposePsEntry)
68
- }
69
-
70
- function normalizeHealth(raw: string): HealthStatus {
71
- if (raw === 'healthy' || raw === 'unhealthy' || raw === 'starting') {
72
- return raw
73
- }
74
-
75
- return 'n-a'
76
- }
77
-
78
- export function parseComposePs(entries: ComposePsEntry[]): ServiceRow[] {
79
- return entries.map(entry => ({
80
- service: entry.Name,
81
- state: entry.State,
82
- // Workspace has no upstream healthcheck in v1; health will upgrade automatically
83
- // when upstream adds one and docker compose ps starts reporting it.
84
- health: normalizeHealth(entry.Health),
85
- }))
86
- }
87
-
88
- export function isAllRunning(rows: ServiceRow[]): boolean {
89
- return rows.every(row => row.state === 'running')
90
- }
91
-
92
- // ─── SSH-backed status fetch ──────────────────────────────────────────────────
93
-
94
- function defaultSpawn(
95
- cmd: string[],
96
- opts: {env: Record<string, string>; stdout: 'pipe'; stderr: 'pipe'},
97
- ): ReturnType<SpawnFn> {
98
- return Bun.spawn(cmd, opts) as ReturnType<SpawnFn>
99
- }
100
-
101
- export async function getGatewayComposeStatus(
102
- host: string,
103
- spawn: SpawnFn = defaultSpawn,
104
- ): Promise<GatewayStatusResult> {
105
- validateGatewayHost(host)
106
-
107
- const knownHostsArgs = buildKnownHostsArgs()
108
- const {args: identityArgs, cleanup} = buildIdentityArgs(process.env.GATEWAY_SSH_KEY)
109
-
110
- const sshCmd = [
111
- 'ssh',
112
- '-o',
113
- 'BatchMode=yes',
114
- '-o',
115
- 'StrictHostKeyChecking=yes',
116
- ...knownHostsArgs,
117
- ...identityArgs,
118
- `root@${host}`,
119
- `docker compose --project-directory ${COMPOSE_PROJECT_DIR} ps --format json`,
120
- ]
121
-
122
- const env: Record<string, string> = {
123
- PATH: process.env.PATH ?? '/usr/bin:/bin',
124
- HOME: process.env.HOME ?? '/tmp',
125
- ...(process.env.SSH_AUTH_SOCK ? {SSH_AUTH_SOCK: process.env.SSH_AUTH_SOCK} : {}),
126
- }
127
-
128
- let stdoutText: string
129
- let stderrText: string
130
- let exitCode: number
131
-
132
- try {
133
- const child = spawn(sshCmd, {env, stdout: 'pipe', stderr: 'pipe'})
134
- ;[stdoutText, stderrText, exitCode] = await Promise.all([
135
- new Response(child.stdout).text(),
136
- new Response(child.stderr).text(),
137
- child.exited,
138
- ])
139
- } finally {
140
- cleanup()
141
- }
142
-
143
- if (exitCode !== 0) {
144
- const redacted = redactHost(stderrText.trim(), host) || 'unknown error'
145
- return {
146
- ok: false,
147
- services: [],
148
- error: `SSH command failed (exit ${exitCode}): ${redacted}`,
149
- }
150
- }
151
-
152
- let entries: ComposePsEntry[]
153
-
154
- try {
155
- entries = parseComposePsOutput(stdoutText)
156
- } catch {
157
- return {
158
- ok: false,
159
- services: [],
160
- error: `Failed to parse docker compose ps output: ${redactHost(stdoutText.slice(0, 200), host)}`,
161
- }
162
- }
163
-
164
- const services = parseComposePs(entries)
165
- const ok = isAllRunning(services)
166
-
167
- return {ok, services}
168
- }
169
-
170
- // ─── Unified status aggregator export ────────────────────────────────────────
171
-
172
- export async function getGatewayStatusSummary(host: string): Promise<StatusSummary> {
173
- const result = await getGatewayComposeStatus(host)
174
-
175
- if (!result.ok || result.services.length === 0) {
176
- const errorMsg = result.error ?? 'No services reported'
177
- return {
178
- app: 'gateway',
179
- http: `ERROR: ${errorMsg}`,
180
- lastDeploy: '—',
181
- version: '—',
182
- contentHash: '—',
183
- usageStats: '—',
184
- }
185
- }
186
-
187
- const rows = result.services.map(s => `${s.service}:${s.state}/${s.health}`).join(', ')
188
-
189
- return {
190
- app: 'gateway',
191
- http: `OK: ${rows}`,
192
- lastDeploy: '—',
193
- version: '—',
194
- contentHash: '—',
195
- usageStats: '—',
196
- }
197
- }
198
-
199
- // ─── Action (exported for direct testing) ────────────────────────────────────
200
-
201
- export async function gatewayStatusAction(
202
- options: {key?: string | undefined},
203
- ctx: ActionCtx,
204
- spawn?: SpawnFn,
205
- ): Promise<void> {
206
- const hostEnvKey = options.key ?? 'GATEWAY_HOST'
207
- const host = process.env[hostEnvKey]
208
-
209
- if (!host) {
210
- ctx.console.error(`Gateway host not set. Export ${hostEnvKey} or pass --key <env-name> pointing to a set variable.`)
211
- ctx.process.exit(1)
212
- return
213
- }
214
-
215
- ctx.console.log('Gateway status')
216
- ctx.console.log('')
217
-
218
- const result = await getGatewayComposeStatus(host, spawn)
219
-
220
- if (!result.ok && result.services.length === 0) {
221
- ctx.console.error(`Error: ${result.error ?? 'Unknown error'}`)
222
- ctx.process.exit(1)
223
- return
224
- }
225
-
226
- ctx.console.log('Service State Health')
227
- ctx.console.log('─────────────────────────────────────')
228
-
229
- for (const row of result.services) {
230
- const svc = row.service.padEnd(16)
231
- const state = row.state.padEnd(10)
232
- ctx.console.log(`${svc} ${state} ${row.health}`)
233
- }
234
-
235
- ctx.console.log('')
236
-
237
- if (result.ok) {
238
- ctx.console.log('Status: OK')
239
- } else {
240
- ctx.console.log('Status: DEGRADED (one or more services not running)')
241
- ctx.process.exit(1)
242
- }
243
- }
244
-
245
- // ─── Command registration ─────────────────────────────────────────────────────
246
-
247
- export function registerGatewayStatus(cli: ReturnType<typeof goke>): void {
248
- cli
249
- .command('gateway status', 'Show operational health of the gateway deployment via docker compose ps.')
250
- .option(
251
- '--key [key]',
252
- z.string().describe('Environment variable name holding the SSH host. Falls back to GATEWAY_HOST when omitted.'),
253
- )
254
- .action((options, ctx) => gatewayStatusAction(options, ctx))
255
- }
@@ -1,177 +0,0 @@
1
- import {resolve} from 'node:path'
2
- import {afterEach, beforeEach, describe, expect, it, spyOn} from 'bun:test'
3
-
4
- import {getLocalDeployEnv, resolveDeployScriptPath, resolveDistIndexPath, validateRemotePreconditions} from './deploy'
5
-
6
- const cliDir = resolve(import.meta.dir, '../../..')
7
-
8
- const envKeys = ['HOST', 'HOME', 'PATH', 'REMOTE_USER', 'SITE_DIR', 'SSH_AUTH_SOCK'] as const
9
-
10
- type ManagedEnvKey = (typeof envKeys)[number]
11
-
12
- let originalEnv: Partial<Record<ManagedEnvKey, string | undefined>>
13
-
14
- function restoreManagedEnv(): void {
15
- for (const key of envKeys) {
16
- const value = originalEnv[key]
17
-
18
- if (value === undefined) {
19
- delete process.env[key]
20
- continue
21
- }
22
-
23
- process.env[key] = value
24
- }
25
- }
26
-
27
- function setManagedEnv(overrides: Partial<Record<ManagedEnvKey, string | undefined>>): void {
28
- restoreManagedEnv()
29
-
30
- for (const [key, value] of Object.entries(overrides)) {
31
- if (value === undefined) {
32
- delete process.env[key]
33
- continue
34
- }
35
-
36
- process.env[key] = value
37
- }
38
- }
39
-
40
- async function runDeployCommand(
41
- args: string[],
42
- envOverrides: Partial<Record<ManagedEnvKey, string | undefined>> = {},
43
- ): Promise<{stdout: string; stderr: string; exitCode: number}> {
44
- const env = {...process.env}
45
-
46
- for (const [key, value] of Object.entries(envOverrides)) {
47
- if (value === undefined) {
48
- delete env[key]
49
- continue
50
- }
51
-
52
- env[key] = value
53
- }
54
-
55
- const proc = Bun.spawn(['bun', 'src/cli.ts', 'keeweb', 'deploy', ...args], {
56
- cwd: cliDir,
57
- env: {
58
- ...env,
59
- NO_COLOR: '1',
60
- SSH_AUTH_SOCK: env.SSH_AUTH_SOCK ?? '/tmp/test-sock',
61
- },
62
- stdout: 'pipe',
63
- stderr: 'pipe',
64
- })
65
-
66
- const [stdout, stderr, exitCode] = await Promise.all([
67
- new Response(proc.stdout).text(),
68
- new Response(proc.stderr).text(),
69
- proc.exited,
70
- ])
71
-
72
- return {stdout, stderr, exitCode}
73
- }
74
-
75
- describe('keeweb deploy', () => {
76
- beforeEach(() => {
77
- originalEnv = Object.fromEntries(envKeys.map(key => [key, process.env[key]]))
78
- })
79
-
80
- afterEach(() => {
81
- restoreManagedEnv()
82
- })
83
-
84
- describe('resolveDeployScriptPath', () => {
85
- it('returns a string path containing deploy.sh', () => {
86
- const deployScriptPath = resolveDeployScriptPath()
87
-
88
- expect(typeof deployScriptPath).toBe('string')
89
- expect(deployScriptPath).toContain('deploy.sh')
90
- })
91
- })
92
-
93
- describe('resolveDistIndexPath', () => {
94
- it('returns a string path ending with dist/index.html', () => {
95
- const distIndexPath = resolveDistIndexPath()
96
-
97
- expect(typeof distIndexPath).toBe('string')
98
- expect(distIndexPath).toEndWith('dist/index.html')
99
- })
100
- })
101
-
102
- describe('getLocalDeployEnv', () => {
103
- it('returns the expected deploy environment when required variables are present', () => {
104
- setManagedEnv({
105
- HOME: '/tmp/test-home',
106
- PATH: '/usr/bin:/bin',
107
- SSH_AUTH_SOCK: '/tmp/test-sock',
108
- })
109
-
110
- expect(getLocalDeployEnv()).toEqual({
111
- HOME: '/tmp/test-home',
112
- HOST: 'box.heatvision.co',
113
- PATH: '/usr/bin:/bin',
114
- REMOTE_USER: 'deploy-kw',
115
- SITE_DIR: '/home/user-data/www/kw.igg.ms',
116
- SSH_AUTH_SOCK: '/tmp/test-sock',
117
- })
118
- })
119
-
120
- it('throws when SSH_AUTH_SOCK is missing', () => {
121
- setManagedEnv({
122
- HOME: '/tmp/test-home',
123
- PATH: '/usr/bin:/bin',
124
- SSH_AUTH_SOCK: undefined,
125
- })
126
-
127
- expect(() => getLocalDeployEnv()).toThrow(/SSH_AUTH_SOCK/)
128
- })
129
-
130
- it('throws when PATH is missing', () => {
131
- setManagedEnv({
132
- HOME: '/tmp/test-home',
133
- PATH: undefined,
134
- SSH_AUTH_SOCK: '/tmp/test-sock',
135
- })
136
-
137
- expect(() => getLocalDeployEnv()).toThrow(/PATH/)
138
- })
139
- })
140
-
141
- describe('validateRemotePreconditions', () => {
142
- it('does not throw when gh is available', () => {
143
- const whichSpy = spyOn(Bun, 'which').mockReturnValue('/opt/homebrew/bin/gh')
144
-
145
- expect(() => validateRemotePreconditions()).not.toThrow()
146
-
147
- whichSpy.mockRestore()
148
- })
149
-
150
- it('throws when gh is unavailable', () => {
151
- const whichSpy = spyOn(Bun, 'which').mockReturnValue(null)
152
-
153
- expect(() => validateRemotePreconditions()).toThrow(/gh CLI is required/)
154
-
155
- whichSpy.mockRestore()
156
- })
157
- })
158
-
159
- describe('CLI flag interactions', () => {
160
- it('rejects --nginx without --local', async () => {
161
- const {stdout, stderr, exitCode} = await runDeployCommand(['--nginx'])
162
-
163
- expect(exitCode).not.toBe(0)
164
- expect(`${stdout}${stderr}`).toContain('only valid with --local')
165
- })
166
-
167
- it('prints the local dry-run plan without executing deploy.sh', async () => {
168
- const deployScriptPath = resolveDeployScriptPath()
169
- const {stdout, stderr, exitCode} = await runDeployCommand(['--local', '--dry-run'])
170
-
171
- expect(exitCode).toBe(0)
172
- expect(stderr).toBe('')
173
- expect(stdout).toContain('Dry run: local KeeWeb deploy')
174
- expect(stdout).toContain(deployScriptPath)
175
- })
176
- })
177
- })
@@ -1,191 +0,0 @@
1
- import type {goke} from 'goke'
2
- import {existsSync} from 'node:fs'
3
- import {resolve} from 'node:path'
4
- import {z} from 'zod'
5
-
6
- const REPO = 'marcusrbrown/infra'
7
- const WORKFLOW_NAME = 'Deploy KeeWeb'
8
- const WORKFLOW_URL = 'https://github.com/marcusrbrown/infra/actions/workflows/deploy-keeweb.yaml'
9
-
10
- const DEFAULT_HOST = 'box.heatvision.co'
11
- const DEFAULT_REMOTE_USER = 'deploy-kw'
12
- const DEFAULT_SITE_DIR = '/home/user-data/www/kw.igg.ms'
13
-
14
- type CliInstance = ReturnType<typeof goke>
15
-
16
- export function resolveDeployScriptPath(): string {
17
- const primary = resolve(import.meta.dir, '../../../../../apps/keeweb/deploy.sh')
18
-
19
- if (existsSync(primary)) {
20
- return primary
21
- }
22
-
23
- return resolve(import.meta.dir, '../../../../apps/keeweb/deploy.sh')
24
- }
25
-
26
- export function resolveDistIndexPath(): string {
27
- return resolve(import.meta.dir, '../../../../../apps/keeweb/dist/index.html')
28
- }
29
-
30
- export function getLocalDeployEnv(): Record<string, string> {
31
- const path = process.env.PATH
32
- const home = process.env.HOME
33
- const sshAuthSock = process.env.SSH_AUTH_SOCK
34
-
35
- if (!path) {
36
- throw new Error('PATH is required for local deploy')
37
- }
38
-
39
- if (!home) {
40
- throw new Error('HOME is required for local deploy')
41
- }
42
-
43
- if (!sshAuthSock) {
44
- throw new Error('SSH_AUTH_SOCK is required for local deploy. Start ssh-agent and load your deploy key first.')
45
- }
46
-
47
- return {
48
- HOST: process.env.HOST ?? DEFAULT_HOST,
49
- REMOTE_USER: process.env.REMOTE_USER ?? DEFAULT_REMOTE_USER,
50
- SITE_DIR: process.env.SITE_DIR ?? DEFAULT_SITE_DIR,
51
- SSH_AUTH_SOCK: sshAuthSock,
52
- PATH: path,
53
- HOME: home,
54
- }
55
- }
56
-
57
- export function validateLocalPreconditions(options: {nginx?: boolean}): {
58
- deployScriptPath: string
59
- distIndexPath: string
60
- } {
61
- const deployScriptPath = resolveDeployScriptPath()
62
- if (!existsSync(deployScriptPath)) {
63
- throw new Error(`deploy.sh not found at expected path: ${deployScriptPath}`)
64
- }
65
-
66
- const distIndexPath = resolveDistIndexPath()
67
- if (!existsSync(distIndexPath)) {
68
- throw new Error(`Local deploy requires built assets. Missing: ${distIndexPath}`)
69
- }
70
-
71
- getLocalDeployEnv()
72
-
73
- if (options.nginx) {
74
- // no-op placeholder to keep validation branch explicit
75
- }
76
-
77
- return {deployScriptPath, distIndexPath}
78
- }
79
-
80
- export function validateRemotePreconditions(): void {
81
- if (!Bun.which('gh')) {
82
- throw new Error('gh CLI is required for remote deploy. Install gh and run `gh auth login`.')
83
- }
84
- }
85
-
86
- export function registerKeewebDeploy(cli: CliInstance): void {
87
- cli
88
- .command(
89
- 'keeweb deploy',
90
- 'Trigger KeeWeb deployment. Default mode dispatches the GitHub Deploy KeeWeb workflow. Use --local to run apps/keeweb/deploy.sh directly from this repo.',
91
- )
92
- .option(
93
- '--local',
94
- z
95
- .boolean()
96
- .default(false)
97
- .describe('Run local deployment using apps/keeweb/deploy.sh instead of triggering GitHub Actions.'),
98
- )
99
- .option(
100
- '--nginx',
101
- z
102
- .boolean()
103
- .default(false)
104
- .describe(
105
- 'Include nginx config deployment. Valid only with --local and passed through to deploy.sh as --nginx.',
106
- ),
107
- )
108
- .option(
109
- '--dry-run',
110
- z
111
- .boolean()
112
- .default(false)
113
- .describe(
114
- 'Print planned actions without validating preconditions, executing deploy.sh, or triggering GitHub Actions.',
115
- ),
116
- )
117
- .example('# Trigger GitHub Deploy KeeWeb workflow (default mode)')
118
- .example('infra keeweb deploy')
119
- .example('# Preview local deploy plan without side effects')
120
- .example('infra keeweb deploy --local --dry-run')
121
- .example('# Run local deploy including nginx config update')
122
- .example('infra keeweb deploy --local --nginx')
123
- .action(async options => {
124
- if (options.nginx && !options.local) {
125
- throw new Error('--nginx is only valid with --local')
126
- }
127
-
128
- if (options.local) {
129
- if (options.dryRun) {
130
- const deployScriptPath = resolveDeployScriptPath()
131
- const args = ['bash', deployScriptPath]
132
- if (options.nginx) {
133
- args.push('--nginx')
134
- }
135
- console.log('Dry run: local KeeWeb deploy')
136
- console.log(`- deploy script: ${deployScriptPath}`)
137
- console.log(`- dist check: ${resolveDistIndexPath()}`)
138
- console.log(`- command: ${args.join(' ')}`)
139
- return
140
- }
141
-
142
- const {deployScriptPath} = validateLocalPreconditions({nginx: options.nginx})
143
- const env = getLocalDeployEnv()
144
- const args = ['bash', deployScriptPath]
145
-
146
- if (options.nginx) {
147
- args.push('--nginx')
148
- }
149
-
150
- const child = Bun.spawn(args, {
151
- env,
152
- stdout: 'inherit',
153
- stderr: 'inherit',
154
- })
155
-
156
- const exitCode = await child.exited
157
- if (exitCode !== 0) {
158
- throw new Error(`Local deploy failed with exit code ${exitCode}`)
159
- }
160
-
161
- return
162
- }
163
-
164
- if (options.dryRun) {
165
- console.log('Dry run: remote KeeWeb deploy')
166
- console.log(`- command: gh workflow run ${WORKFLOW_NAME} --repo ${REPO}`)
167
- console.log(`- workflow URL: ${WORKFLOW_URL}`)
168
- return
169
- }
170
-
171
- validateRemotePreconditions()
172
-
173
- console.warn(
174
- 'Warning: the Deploy KeeWeb workflow includes nginx config deployment as part of workflow_dispatch logic.',
175
- )
176
- console.warn('Warning: the workflow requires keeweb environment approval before jobs execute.')
177
-
178
- const child = Bun.spawn(['gh', 'workflow', 'run', WORKFLOW_NAME, '--repo', REPO], {
179
- stdout: 'inherit',
180
- stderr: 'inherit',
181
- })
182
-
183
- const exitCode = await child.exited
184
- if (exitCode !== 0) {
185
- throw new Error(`Failed to trigger Deploy KeeWeb workflow (exit code ${exitCode})`)
186
- }
187
-
188
- console.log(`Workflow triggered: ${WORKFLOW_URL}`)
189
- console.log('Approve the keeweb environment deployment in GitHub Actions to continue.')
190
- })
191
- }
@@ -1,11 +0,0 @@
1
- import type {goke} from 'goke'
2
-
3
- import {registerKeewebDeploy} from './deploy'
4
- import {registerKeewebOpen} from './open'
5
- import {registerKeewebStatus} from './status'
6
-
7
- export function registerKeewebCommands(cli: ReturnType<typeof goke>): void {
8
- registerKeewebStatus(cli)
9
- registerKeewebDeploy(cli)
10
- registerKeewebOpen(cli)
11
- }
@@ -1,54 +0,0 @@
1
- import {resolve} from 'node:path'
2
- import {describe, expect, it} from 'bun:test'
3
-
4
- import {getOpenerCommand, KEEWEB_URL} from './open'
5
-
6
- const cliDir = resolve(import.meta.dir, '../../..')
7
-
8
- describe('keeweb open', () => {
9
- describe('getOpenerCommand', () => {
10
- it('returns "open" on darwin', () => {
11
- const originalPlatform = process.platform
12
- Object.defineProperty(process, 'platform', {value: 'darwin', configurable: true})
13
-
14
- expect(getOpenerCommand()).toBe('open')
15
-
16
- Object.defineProperty(process, 'platform', {value: originalPlatform, configurable: true})
17
- })
18
-
19
- it('returns "xdg-open" on linux', () => {
20
- const originalPlatform = process.platform
21
- Object.defineProperty(process, 'platform', {value: 'linux', configurable: true})
22
-
23
- expect(getOpenerCommand()).toBe('xdg-open')
24
-
25
- Object.defineProperty(process, 'platform', {value: originalPlatform, configurable: true})
26
- })
27
- })
28
-
29
- describe('KEEWEB_URL', () => {
30
- it('is the expected URL', () => {
31
- expect(KEEWEB_URL).toBe('https://kw.igg.ms/')
32
- })
33
- })
34
-
35
- describe('CLI help', () => {
36
- it('prints help text', async () => {
37
- const proc = Bun.spawn(['bun', 'src/cli.ts', 'keeweb', 'open', '--help'], {
38
- cwd: cliDir,
39
- env: {...process.env, NO_COLOR: '1'},
40
- stdout: 'pipe',
41
- stderr: 'pipe',
42
- })
43
-
44
- const [stdout, , exitCode] = await Promise.all([
45
- new Response(proc.stdout).text(),
46
- new Response(proc.stderr).text(),
47
- proc.exited,
48
- ])
49
-
50
- expect(exitCode).toBe(0)
51
- expect(stdout).toContain('Open KeeWeb in the default browser')
52
- })
53
- })
54
- })