Skip to content

Commit 6670cb9

Browse files
committed
feat: include doctor command
1 parent d4ae297 commit 6670cb9

File tree

6 files changed

+183
-10
lines changed

6 files changed

+183
-10
lines changed

src/__tests__/cli-commands.test.ts

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/* eslint-env jest */
2+
3+
import { getVersion, runDoctor } from '../cli-commands.js'
4+
import { getPackageJson } from '../utils.js'
5+
import { APIHealthResponse } from '../types.js'
6+
import nock from 'nock'
7+
8+
const pkg = getPackageJson()
9+
10+
describe('CLI Commands', () => {
11+
describe('getVersion', () => {
12+
it('should return the correct version information', () => {
13+
const result = getVersion()
14+
15+
expect(result).toEqual({
16+
messages: [`Your version is: ${pkg.name}@${pkg.version} (${pkg.license})`],
17+
success: true
18+
})
19+
})
20+
21+
it('should include the package name, version and license', () => {
22+
const result = getVersion()
23+
const message = result.messages[0]
24+
25+
expect(message).toContain(pkg.name)
26+
expect(message).toContain(pkg.version)
27+
expect(message).toContain(pkg.license)
28+
})
29+
})
30+
31+
describe('runDoctor', () => {
32+
let apiHealthResponse: APIHealthResponse
33+
beforeEach(() => {
34+
nock.cleanAll()
35+
apiHealthResponse = {
36+
status: 'ok',
37+
timestamp: new Date().toISOString(),
38+
version: '0.1.0-beta3',
39+
name: 'visionBoard'
40+
}
41+
})
42+
43+
it('should return success when API is available and compatible', async () => {
44+
nock('http://localhost:3000')
45+
.get('/api/v1/__health')
46+
.reply(200, apiHealthResponse)
47+
48+
const result = await runDoctor()
49+
50+
expect(result.success).toBe(true)
51+
expect(result.messages).toContain('✅ API is available and compatible')
52+
expect(result.messages).toHaveLength(1)
53+
})
54+
55+
it('should return failure when API is not available (due connection error)', async () => {
56+
nock('http://localhost:3000')
57+
.get('/api/v1/__health')
58+
.reply(500)
59+
60+
const result = await runDoctor()
61+
62+
expect(result.success).toBe(false)
63+
expect(result.messages).toContain('❌ Seems like the API is not available')
64+
expect(result.messages).toHaveLength(1)
65+
})
66+
67+
it('should return failure when API is not available (due internal error)', async () => {
68+
nock('http://localhost:3000')
69+
.get('/api/v1/__health')
70+
.reply(200, { ...apiHealthResponse, status: 'error' })
71+
72+
const result = await runDoctor()
73+
74+
expect(result.success).toBe(false)
75+
expect(result.messages).toContain('❌ Seems like the API is not available')
76+
expect(result.messages).toHaveLength(1)
77+
})
78+
79+
it('should return failure when API version is not compatible', async () => {
80+
nock('http://localhost:3000')
81+
.get('/api/v1/__health')
82+
.reply(200, { ...apiHealthResponse, version: '0.1.0-beta4' })
83+
84+
const result = await runDoctor()
85+
86+
expect(result.success).toBe(false)
87+
expect(result.messages).toContain('❌ API version is not compatible')
88+
expect(result.messages).toHaveLength(1)
89+
})
90+
})
91+
})

src/api-client.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { getConfig } from './utils.js'
2+
import { got } from 'got'
3+
import { APIHealthResponse } from './types.js'
4+
5+
export const apiClient = () => {
6+
const config = getConfig()
7+
return got.extend({
8+
prefixUrl: `${config.visionBoardInstanceUrl}/api/v1`
9+
})
10+
}
11+
12+
export const getAPIDetails = async (): Promise<APIHealthResponse> => {
13+
const client = apiClient()
14+
const response = await client.get('__health', { responseType: 'json' })
15+
if (response.statusCode !== 200) {
16+
throw new Error('Failed to get the data from the API')
17+
}
18+
return response.body as APIHealthResponse
19+
}

src/cli-commands.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { CommandResult } from './types.js'
2+
import { isApiAvailable, isApiCompatible, getPackageJson } from './utils.js'
3+
import { getAPIDetails } from './api-client.js'
4+
5+
const pkg = getPackageJson()
6+
7+
export const getVersion = (): CommandResult => {
8+
return {
9+
messages: [`Your version is: ${pkg.name}@${pkg.version} (${pkg.license})`],
10+
success: true
11+
}
12+
}
13+
14+
export const runDoctor = async (): Promise<CommandResult> => {
15+
const messages: string[] = []
16+
let success = true
17+
try {
18+
const details = await getAPIDetails()
19+
20+
if (!isApiAvailable(details)) {
21+
messages.push('❌ Seems like the API is not available')
22+
success = false
23+
} else if (!isApiCompatible(details)) {
24+
messages.push('❌ API version is not compatible')
25+
success = false
26+
} else {
27+
messages.push('✅ API is available and compatible')
28+
}
29+
} catch (error) {
30+
messages.push('❌ Seems like the API is not available')
31+
success = false
32+
}
33+
34+
return {
35+
messages,
36+
success
37+
}
38+
}

src/index.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
11
#!/usr/bin/env node
22

33
import { Command } from 'commander'
4-
import { fileURLToPath } from 'url'
5-
import { dirname, join } from 'path'
6-
import { readFileSync } from 'fs'
74

8-
const __filename = fileURLToPath(import.meta.url)
9-
const __dirname = dirname(__filename)
10-
const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf8'))
5+
import { handleCommandResult } from './utils.js'
6+
import { getVersion, runDoctor } from './cli-commands.js'
117

128
const program = new Command()
139

1410
program
1511
.name('visionboard')
1612
.command('version')
1713
.description('Show version information')
18-
.action(() => {
19-
console.log(`Your version is: ${pkg.name}@${pkg.version} (${pkg.license})`)
14+
.action(() => handleCommandResult(getVersion()))
15+
16+
program
17+
.command('doctor')
18+
.description('Check compatibility and availability with the API')
19+
.action(async () => {
20+
console.log('Checking API availability...')
21+
const result = await runDoctor()
22+
handleCommandResult(result)
2023
})
2124

2225
program.parse(process.argv)

src/types.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,7 @@ export interface Config {
1919
visionBoardInstanceUrl: string
2020
}
2121

22-
// Add more interfaces as needed
22+
export interface CommandResult {
23+
messages: string[]
24+
success: boolean
25+
}

src/utils.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
import validator from 'validator'
2-
import { Config, APIHealthResponse } from './types.js'
2+
import { Config, APIHealthResponse, CommandResult } from './types.js'
3+
import { readFileSync } from 'fs'
4+
import { fileURLToPath } from 'url'
5+
import { dirname, join } from 'path'
6+
7+
const __filename = fileURLToPath(import.meta.url)
8+
const __dirname = dirname(__filename)
9+
10+
export const getPackageJson = () => {
11+
return JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf8'))
12+
}
313

414
export const getConfig = (): Config => {
515
const envUrl = process.env.VISIONBOARD_INSTANCE_URL
@@ -24,3 +34,12 @@ export const isApiCompatible = (details: APIHealthResponse) => {
2434
export const isApiAvailable = (details: APIHealthResponse) => {
2535
return details.status === 'ok'
2636
}
37+
38+
export const handleCommandResult = (result: CommandResult) => {
39+
if (result.success) {
40+
console.log(result.messages.join('\n'))
41+
} else {
42+
console.error(result.messages.join('\n'))
43+
process.exit(1)
44+
}
45+
}

0 commit comments

Comments
 (0)