1- import fs from 'fs' ;
2- import path from 'path' ;
3-
4- import { getEmbeddingProvider , embeddingsEnabled } from '../../lib/embeddings' ;
1+ import { getHealthReport , isHealthy , HealthCheck } from '../../core/health' ;
52import { handleError } from '../shared/utils' ;
63
74type ClercModule = typeof import ( 'clerc' ) ;
@@ -10,8 +7,6 @@ type HealthFlags = {
107 json ?: boolean ;
118} ;
129
13- const DEFAULT_DB_PATH = 'forest.db' ;
14-
1510export function createHealthCommand ( clerc : ClercModule ) {
1611 return clerc . defineCommand (
1712 {
@@ -34,32 +29,8 @@ export function createHealthCommand(clerc: ClercModule) {
3429 ) ;
3530}
3631
37- type HealthCheck = {
38- status : 'ok' | 'warning' | 'error' ;
39- message : string ;
40- } ;
41-
42- type HealthReport = {
43- database : HealthCheck & { path ?: string ; sizeBytes ?: number } ;
44- embeddingProvider : HealthCheck & { provider ?: string } ;
45- openaiKey ?: HealthCheck ;
46- localTransformer ?: HealthCheck ;
47- } ;
48-
4932async function runHealth ( flags : HealthFlags ) {
50- const report : HealthReport = {
51- database : await checkDatabase ( ) ,
52- embeddingProvider : await checkEmbeddingProvider ( ) ,
53- } ;
54-
55- const provider = getEmbeddingProvider ( ) ;
56- if ( provider === 'openai' ) {
57- report . openaiKey = checkOpenAIKey ( ) ;
58- }
59-
60- if ( provider === 'local' ) {
61- report . localTransformer = await checkLocalTransformer ( ) ;
62- }
33+ const report = await getHealthReport ( ) ;
6334
6435 if ( flags . json ) {
6536 console . log ( JSON . stringify ( report , null , 2 ) ) ;
@@ -95,14 +66,7 @@ async function runHealth(flags: HealthFlags) {
9566 console . log ( '' ) ;
9667 }
9768
98- const allOk = [
99- report . database . status === 'ok' ,
100- report . embeddingProvider . status === 'ok' ,
101- ! report . openaiKey || report . openaiKey . status === 'ok' ,
102- ! report . localTransformer || report . localTransformer . status === 'ok' ,
103- ] . every ( Boolean ) ;
104-
105- if ( allOk ) {
69+ if ( isHealthy ( report ) ) {
10670 console . log ( '✔ All systems operational' ) ;
10771 } else {
10872 console . log ( '⚠ Some checks failed. Review the output above.' ) ;
@@ -114,155 +78,3 @@ function printCheck(label: string, check: HealthCheck) {
11478 const icon = check . status === 'ok' ? '✔' : check . status === 'warning' ? '⚠' : '✖' ;
11579 console . log ( `${ icon } ${ label } : ${ check . message } ` ) ;
11680}
117-
118- async function checkDatabase ( ) : Promise < HealthCheck & { path ?: string ; sizeBytes ?: number } > {
119- const dbPath = process . env . FOREST_DB_PATH ?? DEFAULT_DB_PATH ;
120- const resolvedPath = path . resolve ( dbPath ) ;
121-
122- if ( ! fs . existsSync ( resolvedPath ) ) {
123- return {
124- status : 'warning' ,
125- message : 'Database file does not exist yet (will be created on first capture)' ,
126- path : resolvedPath ,
127- } ;
128- }
129-
130- try {
131- const stats = fs . statSync ( resolvedPath ) ;
132- if ( ! stats . isFile ( ) ) {
133- return {
134- status : 'error' ,
135- message : 'Database path exists but is not a file' ,
136- path : resolvedPath ,
137- } ;
138- }
139-
140- // Try to read the first few bytes to verify it's accessible
141- const fd = fs . openSync ( resolvedPath , 'r' ) ;
142- const buffer = Buffer . alloc ( 16 ) ;
143- fs . readSync ( fd , buffer , 0 , 16 , 0 ) ;
144- fs . closeSync ( fd ) ;
145-
146- // Check if it looks like a SQLite file (magic: "SQLite format 3\0")
147- const magic = buffer . toString ( 'utf-8' , 0 , 15 ) ;
148- if ( ! magic . startsWith ( 'SQLite format 3' ) ) {
149- return {
150- status : 'warning' ,
151- message : 'File exists but may not be a valid SQLite database' ,
152- path : resolvedPath ,
153- sizeBytes : stats . size ,
154- } ;
155- }
156-
157- return {
158- status : 'ok' ,
159- message : 'Database file is accessible and valid' ,
160- path : resolvedPath ,
161- sizeBytes : stats . size ,
162- } ;
163- } catch ( error ) {
164- return {
165- status : 'error' ,
166- message : `Cannot access database file: ${ error instanceof Error ? error . message : String ( error ) } ` ,
167- path : resolvedPath ,
168- } ;
169- }
170- }
171-
172- async function checkEmbeddingProvider ( ) : Promise < HealthCheck & { provider ?: string } > {
173- const provider = getEmbeddingProvider ( ) ;
174- const enabled = embeddingsEnabled ( ) ;
175-
176- if ( ! enabled ) {
177- return {
178- status : 'warning' ,
179- message : 'Embeddings are disabled (pure lexical scoring)' ,
180- provider,
181- } ;
182- }
183-
184- if ( provider === 'mock' ) {
185- return {
186- status : 'warning' ,
187- message : 'Using mock embeddings (deterministic, non-semantic)' ,
188- provider,
189- } ;
190- }
191-
192- if ( provider === 'openai' ) {
193- return {
194- status : 'ok' ,
195- message : 'Using OpenAI embeddings' ,
196- provider,
197- } ;
198- }
199-
200- if ( provider === 'local' ) {
201- return {
202- status : 'ok' ,
203- message : 'Using local transformer embeddings' ,
204- provider,
205- } ;
206- }
207-
208- return {
209- status : 'ok' ,
210- message : `Provider: ${ provider } ` ,
211- provider,
212- } ;
213- }
214-
215- function checkOpenAIKey ( ) : HealthCheck {
216- const apiKey = process . env . OPENAI_API_KEY ;
217-
218- if ( ! apiKey ) {
219- return {
220- status : 'error' ,
221- message : 'OPENAI_API_KEY is not set (required for OpenAI provider)' ,
222- } ;
223- }
224-
225- if ( apiKey . trim ( ) . length === 0 ) {
226- return {
227- status : 'error' ,
228- message : 'OPENAI_API_KEY is empty' ,
229- } ;
230- }
231-
232- if ( ! apiKey . startsWith ( 'sk-' ) ) {
233- return {
234- status : 'warning' ,
235- message : 'OPENAI_API_KEY format looks unusual (expected to start with "sk-")' ,
236- } ;
237- }
238-
239- return {
240- status : 'ok' ,
241- message : 'OPENAI_API_KEY is set and format looks valid' ,
242- } ;
243- }
244-
245- async function checkLocalTransformer ( ) : Promise < HealthCheck > {
246- try {
247- // Try to import the transformers module
248- const mod = await import ( '@xenova/transformers' ) ;
249- if ( ! mod || typeof mod . pipeline !== 'function' ) {
250- return {
251- status : 'error' ,
252- message : '@xenova/transformers is installed but pipeline function not found' ,
253- } ;
254- }
255-
256- // We don't actually load the model here to avoid startup delay,
257- // but we verify the package is available
258- return {
259- status : 'ok' ,
260- message : '@xenova/transformers is installed and available' ,
261- } ;
262- } catch ( error ) {
263- return {
264- status : 'error' ,
265- message : `@xenova/transformers is not available: ${ error instanceof Error ? error . message : String ( error ) } ` ,
266- } ;
267- }
268- }
0 commit comments