@onebrain-ai/cli 2.0.0 → 2.0.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.
package/dist/onebrain CHANGED
@@ -10742,7 +10742,7 @@ init_dist();
10742
10742
  import { mkdir as mkdir3, readFile as readFile3, rename as rename3, stat as stat3, writeFile as writeFile3 } from "node:fs/promises";
10743
10743
  import { homedir as homedir3 } from "node:os";
10744
10744
  import { dirname as dirname3, join as join5 } from "node:path";
10745
- var binaryVersion = typeof BUILD_VERSION !== "undefined" ? BUILD_VERSION : "dev";
10745
+ var binaryVersion = "2.0.2";
10746
10746
  var STANDARD_FOLDERS = [
10747
10747
  "00-inbox",
10748
10748
  "01-projects",
@@ -12598,8 +12598,8 @@ async function vaultSyncCommand2(vaultRoot, opts = {}) {
12598
12598
  }
12599
12599
 
12600
12600
  // src/index.ts
12601
- var VERSION = typeof BUILD_VERSION !== "undefined" ? BUILD_VERSION : "0.0.0-dev";
12602
- var RELEASE_DATE = typeof BUILD_DATE !== "undefined" ? BUILD_DATE : "dev";
12601
+ var VERSION = "2.0.2";
12602
+ var RELEASE_DATE = "2026-04-25";
12603
12603
  var VERSION_STRING = `OneBrain v${VERSION} \u2014 released ${RELEASE_DATE}`;
12604
12604
  if (process.argv.slice(2).length === 0) {
12605
12605
  console.log(VERSION_STRING);
package/package.json CHANGED
@@ -1,6 +1,28 @@
1
1
  {
2
2
  "name": "@onebrain-ai/cli",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
+ "description": "CLI for OneBrain — personal AI OS for Obsidian with persistent memory, 24+ skills, and Claude Code integration",
5
+ "keywords": [
6
+ "onebrain",
7
+ "obsidian",
8
+ "ai",
9
+ "cli",
10
+ "memory",
11
+ "knowledge-management",
12
+ "claude",
13
+ "agent",
14
+ "pkm",
15
+ "productivity",
16
+ "vault"
17
+ ],
18
+ "homepage": "https://github.com/kengio/onebrain",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/kengio/onebrain.git"
22
+ },
23
+ "bugs": "https://github.com/kengio/onebrain/issues",
24
+ "license": "MIT",
25
+ "files": ["dist/onebrain"],
4
26
  "type": "module",
5
27
  "bin": {
6
28
  "onebrain": "dist/onebrain"
@@ -11,12 +33,12 @@
11
33
  "bump": "echo 'bump not yet implemented'"
12
34
  },
13
35
  "dependencies": {
14
- "@onebrain/core": "workspace:*",
15
36
  "@clack/prompts": "^0.9",
16
37
  "commander": "^12",
17
38
  "yaml": "^2"
18
39
  },
19
40
  "devDependencies": {
41
+ "@onebrain/core": "workspace:*",
20
42
  "@types/bun": "latest",
21
43
  "@types/node": "^20"
22
44
  }
@@ -1,416 +0,0 @@
1
- /**
2
- * Tests for `onebrain doctor` — runDoctor()
3
- *
4
- * All @onebrain/core validators are injected via opts so tests are
5
- * fast, offline, and deterministic. No mock.module needed.
6
- */
7
-
8
- import { afterEach, beforeEach, describe, expect, it, spyOn } from 'bun:test';
9
- import { mkdir, rm } from 'node:fs/promises';
10
- import { tmpdir } from 'node:os';
11
- import { join } from 'node:path';
12
-
13
- import type { VaultConfig } from '@onebrain/core';
14
- import { type DoctorOptions, runDoctor } from './doctor.js';
15
-
16
- // ---------------------------------------------------------------------------
17
- // Helpers
18
- // ---------------------------------------------------------------------------
19
-
20
- async function makeTempVault(): Promise<string> {
21
- const dir = join(
22
- tmpdir(),
23
- `onebrain-doctor-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
24
- );
25
- await mkdir(dir, { recursive: true });
26
- return dir;
27
- }
28
-
29
- const DEFAULT_CONFIG: VaultConfig = {
30
- folders: {
31
- inbox: '00-inbox',
32
- projects: '01-projects',
33
- areas: '02-areas',
34
- knowledge: '03-knowledge',
35
- resources: '04-resources',
36
- agent: '05-agent',
37
- archive: '06-archive',
38
- logs: '07-logs',
39
- },
40
- };
41
-
42
- function makeAllOkValidators(): Required<
43
- Pick<
44
- DoctorOptions,
45
- | 'checkVaultYmlFn'
46
- | 'loadVaultConfigFn'
47
- | 'checkFoldersFn'
48
- | 'checkHarnessBinaryFn'
49
- | 'checkQmdEmbeddingsFn'
50
- | 'checkVersionDriftFn'
51
- | 'checkOrphanCheckpointsFn'
52
- | 'checkSandboxFn'
53
- >
54
- > {
55
- return {
56
- checkVaultYmlFn: async () => ({ check: 'vault.yml', status: 'ok', message: 'valid' }),
57
- loadVaultConfigFn: async () => DEFAULT_CONFIG,
58
- checkFoldersFn: async () => ({ check: 'folders', status: 'ok', message: '8/8 present' }),
59
- checkHarnessBinaryFn: async () => ({
60
- check: 'runtime.harness',
61
- status: 'ok',
62
- message: 'claude-code (found)',
63
- }),
64
- checkQmdEmbeddingsFn: async () => ({
65
- check: 'qmd-embeddings',
66
- status: 'ok',
67
- message: 'all embedded',
68
- }),
69
- checkVersionDriftFn: async () => ({ check: 'version-drift', status: 'ok', message: 'v1.0.0' }),
70
- checkOrphanCheckpointsFn: async () => ({
71
- check: 'orphan-checkpoints',
72
- status: 'ok',
73
- message: '0 orphans',
74
- }),
75
- checkSandboxFn: async () => ({ check: 'sandbox', status: 'ok', message: 'enabled' }),
76
- };
77
- }
78
-
79
- let tempDir: string;
80
-
81
- beforeEach(async () => {
82
- tempDir = await makeTempVault();
83
- });
84
-
85
- afterEach(async () => {
86
- await rm(tempDir, { recursive: true, force: true });
87
- });
88
-
89
- // ---------------------------------------------------------------------------
90
- // Tests
91
- // ---------------------------------------------------------------------------
92
-
93
- describe('runDoctor', () => {
94
- // ── Exit codes ─────────────────────────────────────────────────────────────
95
-
96
- describe('exit codes', () => {
97
- it('returns exitCode 1 when any check returns status error', async () => {
98
- const validators = makeAllOkValidators();
99
- validators.checkVaultYmlFn = async () => ({
100
- check: 'vault.yml',
101
- status: 'error',
102
- message: 'not found',
103
- });
104
-
105
- const result = await runDoctor({ vaultDir: tempDir, isTTY: false, ...validators });
106
-
107
- expect(result.exitCode).toBe(1);
108
- expect(result.ok).toBe(false);
109
- expect(result.errorCount).toBeGreaterThanOrEqual(1);
110
- });
111
-
112
- it('returns exitCode 0 when checks return only warnings (no errors)', async () => {
113
- const validators = makeAllOkValidators();
114
- validators.checkFoldersFn = async () => ({
115
- check: 'folders',
116
- status: 'warn',
117
- message: '7/8 present',
118
- });
119
- validators.checkSandboxFn = async () => ({
120
- check: 'sandbox',
121
- status: 'warn',
122
- message: 'disabled',
123
- });
124
-
125
- const result = await runDoctor({ vaultDir: tempDir, isTTY: false, ...validators });
126
-
127
- expect(result.exitCode).toBe(0);
128
- expect(result.ok).toBe(true);
129
- expect(result.warningCount).toBeGreaterThanOrEqual(2);
130
- expect(result.errorCount).toBe(0);
131
- });
132
-
133
- it('returns exitCode 0 when all checks pass', async () => {
134
- const result = await runDoctor({ vaultDir: tempDir, isTTY: false, ...makeAllOkValidators() });
135
-
136
- expect(result.exitCode).toBe(0);
137
- expect(result.ok).toBe(true);
138
- expect(result.errorCount).toBe(0);
139
- expect(result.warningCount).toBe(0);
140
- });
141
- });
142
-
143
- // ── binaryVersion forwarding ───────────────────────────────────────────────
144
-
145
- describe('binaryVersion forwarding', () => {
146
- it('forwards binaryVersion to checkVersionDriftFn when provided', async () => {
147
- let capturedBinaryVersion: string | undefined = 'not-set';
148
- const validators = makeAllOkValidators();
149
- validators.checkVersionDriftFn = async (_vaultDir, _config, bv) => {
150
- capturedBinaryVersion = bv;
151
- return { check: 'version-drift', status: 'ok', message: 'ok' };
152
- };
153
-
154
- await runDoctor({ vaultDir: tempDir, isTTY: false, binaryVersion: 'v2.0.0', ...validators });
155
-
156
- expect(capturedBinaryVersion).toBe('v2.0.0');
157
- });
158
-
159
- it('passes undefined binaryVersion to checkVersionDriftFn when omitted', async () => {
160
- let capturedBinaryVersion: string | undefined = 'not-set';
161
- const validators = makeAllOkValidators();
162
- validators.checkVersionDriftFn = async (_vaultDir, _config, bv) => {
163
- capturedBinaryVersion = bv;
164
- return { check: 'version-drift', status: 'ok', message: 'ok' };
165
- };
166
-
167
- await runDoctor({ vaultDir: tempDir, isTTY: false, ...validators });
168
-
169
- expect(capturedBinaryVersion).toBeUndefined();
170
- });
171
- });
172
-
173
- // ── Summary line selection ─────────────────────────────────────────────────
174
-
175
- describe('summary line selection', () => {
176
- it('shows "N errors, N warnings" when both errors and warnings exist', async () => {
177
- const logLines: string[] = [];
178
- const spy = spyOn(console, 'log').mockImplementation((msg: string) => {
179
- logLines.push(msg);
180
- });
181
- try {
182
- const validators = makeAllOkValidators();
183
- validators.checkVaultYmlFn = async () => ({
184
- check: 'vault.yml',
185
- status: 'error',
186
- message: 'not found',
187
- });
188
- validators.checkFoldersFn = async () => ({
189
- check: 'folders',
190
- status: 'warn',
191
- message: '7/8 present',
192
- });
193
- await runDoctor({ vaultDir: tempDir, isTTY: false, ...validators });
194
- } finally {
195
- spy.mockRestore();
196
- }
197
-
198
- expect(logLines.join('\n')).toMatch(/Summary: 1 errors, 1 warnings/);
199
- });
200
-
201
- it('shows "N errors" (no warnings mention) when only errors', async () => {
202
- const logLines: string[] = [];
203
- const spy = spyOn(console, 'log').mockImplementation((msg: string) => {
204
- logLines.push(msg);
205
- });
206
- try {
207
- const validators = makeAllOkValidators();
208
- validators.checkVaultYmlFn = async () => ({
209
- check: 'vault.yml',
210
- status: 'error',
211
- message: 'not found',
212
- });
213
- await runDoctor({ vaultDir: tempDir, isTTY: false, ...validators });
214
- } finally {
215
- spy.mockRestore();
216
- }
217
-
218
- const output = logLines.join('\n');
219
- expect(output).toMatch(/Summary: 1 errors$/m);
220
- expect(output).not.toMatch(/warnings/);
221
- });
222
-
223
- it('shows "N warnings — ok to run" when only warnings (no errors)', async () => {
224
- const logLines: string[] = [];
225
- const spy = spyOn(console, 'log').mockImplementation((msg: string) => {
226
- logLines.push(msg);
227
- });
228
- try {
229
- const validators = makeAllOkValidators();
230
- validators.checkSandboxFn = async () => ({
231
- check: 'sandbox',
232
- status: 'warn',
233
- message: 'disabled',
234
- });
235
- await runDoctor({ vaultDir: tempDir, isTTY: false, ...validators });
236
- } finally {
237
- spy.mockRestore();
238
- }
239
-
240
- expect(logLines.join('\n')).toMatch(/Summary: 1 warnings — ok to run/);
241
- });
242
-
243
- it('shows "All checks passed" when no errors or warnings', async () => {
244
- const logLines: string[] = [];
245
- const spy = spyOn(console, 'log').mockImplementation((msg: string) => {
246
- logLines.push(msg);
247
- });
248
- try {
249
- await runDoctor({ vaultDir: tempDir, isTTY: false, ...makeAllOkValidators() });
250
- } finally {
251
- spy.mockRestore();
252
- }
253
-
254
- expect(logLines.join('\n')).toMatch(/Summary: All checks passed/);
255
- });
256
- });
257
-
258
- // ── TTY vs non-TTY output formatting ──────────────────────────────────────
259
-
260
- describe('TTY vs non-TTY output', () => {
261
- it('non-TTY: plain title without leading blank line', async () => {
262
- const logLines: string[] = [];
263
- const spy = spyOn(console, 'log').mockImplementation((msg: string) => {
264
- logLines.push(msg);
265
- });
266
- try {
267
- await runDoctor({ vaultDir: tempDir, isTTY: false, ...makeAllOkValidators() });
268
- } finally {
269
- spy.mockRestore();
270
- }
271
-
272
- expect(logLines.join('\n')).toMatch(/^OneBrain Doctor 🔍/);
273
- });
274
-
275
- it('TTY: title is padded with surrounding blank lines', async () => {
276
- const logLines: string[] = [];
277
- const spy = spyOn(console, 'log').mockImplementation((msg: string) => {
278
- logLines.push(msg);
279
- });
280
- try {
281
- await runDoctor({ vaultDir: tempDir, isTTY: true, ...makeAllOkValidators() });
282
- } finally {
283
- spy.mockRestore();
284
- }
285
-
286
- const output = logLines.join('\n');
287
- expect(output).toMatch(/^\n\s+OneBrain Doctor 🔍/);
288
- expect(output).toMatch(/Summary: All checks passed\n$/);
289
- });
290
- });
291
-
292
- // ── loadVaultConfig failure resilience ────────────────────────────────────
293
-
294
- describe('loadVaultConfig failure resilience', () => {
295
- it('continues with default config when loadVaultConfigFn throws after valid vault.yml', async () => {
296
- let foldersConfigReceived: VaultConfig | undefined;
297
- const validators = makeAllOkValidators();
298
- validators.checkVaultYmlFn = async () => ({
299
- check: 'vault.yml',
300
- status: 'ok',
301
- message: 'valid',
302
- });
303
- validators.loadVaultConfigFn = async () => {
304
- throw new Error('parse error');
305
- };
306
- validators.checkFoldersFn = async (_vaultDir, config) => {
307
- foldersConfigReceived = config;
308
- return { check: 'folders', status: 'ok', message: '8/8 present' };
309
- };
310
-
311
- const result = await runDoctor({ vaultDir: tempDir, isTTY: false, ...validators });
312
-
313
- expect(result.ok).toBe(true);
314
- expect(result.exitCode).toBe(0);
315
- expect(foldersConfigReceived?.folders.inbox).toBe('00-inbox');
316
- expect(foldersConfigReceived?.folders.logs).toBe('07-logs');
317
- });
318
-
319
- it('skips loadVaultConfigFn when checkVaultYml returns error', async () => {
320
- let loadCalled = false;
321
- const validators = makeAllOkValidators();
322
- validators.checkVaultYmlFn = async () => ({
323
- check: 'vault.yml',
324
- status: 'error',
325
- message: 'not found',
326
- });
327
- validators.loadVaultConfigFn = async () => {
328
- loadCalled = true;
329
- return DEFAULT_CONFIG;
330
- };
331
-
332
- await runDoctor({ vaultDir: tempDir, isTTY: false, ...validators });
333
-
334
- expect(loadCalled).toBe(false);
335
- });
336
- });
337
-
338
- // ── Hint lines ────────────────────────────────────────────────────────────
339
-
340
- describe('hint lines', () => {
341
- it('includes hint line in output when a check returns a hint', async () => {
342
- const logLines: string[] = [];
343
- const spy = spyOn(console, 'log').mockImplementation((msg: string) => {
344
- logLines.push(msg);
345
- });
346
- try {
347
- const validators = makeAllOkValidators();
348
- validators.checkVaultYmlFn = async () => ({
349
- check: 'vault.yml',
350
- status: 'error',
351
- message: 'vault.yml not found',
352
- hint: 'Run onebrain init to create vault.yml',
353
- });
354
- await runDoctor({ vaultDir: tempDir, isTTY: false, ...validators });
355
- } finally {
356
- spy.mockRestore();
357
- }
358
-
359
- expect(logLines.join('\n')).toContain('→ Run onebrain init to create vault.yml');
360
- });
361
-
362
- it('does not include a hint line when check has no hint', async () => {
363
- const logLines: string[] = [];
364
- const spy = spyOn(console, 'log').mockImplementation((msg: string) => {
365
- logLines.push(msg);
366
- });
367
- try {
368
- await runDoctor({ vaultDir: tempDir, isTTY: false, ...makeAllOkValidators() });
369
- } finally {
370
- spy.mockRestore();
371
- }
372
-
373
- expect(logLines.join('\n')).not.toContain('→');
374
- });
375
- });
376
-
377
- // ── errorCount / warningCount accuracy ────────────────────────────────────
378
-
379
- describe('result counts', () => {
380
- it('accurately counts multiple errors and warnings across all checks', async () => {
381
- const validators = makeAllOkValidators();
382
- validators.checkVaultYmlFn = async () => ({
383
- check: 'vault.yml',
384
- status: 'error',
385
- message: 'not found',
386
- });
387
- validators.checkFoldersFn = async () => ({
388
- check: 'folders',
389
- status: 'error',
390
- message: '0/8 present',
391
- });
392
- validators.checkSandboxFn = async () => ({
393
- check: 'sandbox',
394
- status: 'warn',
395
- message: 'disabled',
396
- });
397
- validators.checkHarnessBinaryFn = async () => ({
398
- check: 'runtime.harness',
399
- status: 'warn',
400
- message: 'not found',
401
- });
402
-
403
- const result = await runDoctor({ vaultDir: tempDir, isTTY: false, ...validators });
404
-
405
- expect(result.errorCount).toBe(2);
406
- expect(result.warningCount).toBe(2);
407
- expect(result.exitCode).toBe(1);
408
- });
409
-
410
- it('returns errorCount 0 and warningCount 0 when all checks pass', async () => {
411
- const result = await runDoctor({ vaultDir: tempDir, isTTY: false, ...makeAllOkValidators() });
412
- expect(result.errorCount).toBe(0);
413
- expect(result.warningCount).toBe(0);
414
- });
415
- });
416
- });
@@ -1,203 +0,0 @@
1
- import {
2
- type DoctorResult,
3
- type VaultConfig,
4
- checkFolders,
5
- checkHarnessBinary,
6
- checkOrphanCheckpoints,
7
- checkQmdEmbeddings,
8
- checkSandbox,
9
- checkVaultYml,
10
- checkVersionDrift,
11
- loadVaultConfig,
12
- } from '@onebrain/core';
13
-
14
- // ---------------------------------------------------------------------------
15
- // Types
16
- // ---------------------------------------------------------------------------
17
-
18
- export interface DoctorOptions {
19
- /** Vault root directory (default: process.cwd()). */
20
- vaultDir?: string;
21
- /** Whether stdout is a TTY (default: process.stdout.isTTY). */
22
- isTTY?: boolean;
23
- /** Compiled binary version (BUILD_VERSION). When provided, compared against plugin.json instead of vault.yml onebrain_version. */
24
- binaryVersion?: string;
25
- /** Injectable validators — real implementations are used when absent. */
26
- checkVaultYmlFn?: (vaultDir: string) => Promise<DoctorResult>;
27
- loadVaultConfigFn?: (vaultDir: string) => Promise<VaultConfig>;
28
- checkFoldersFn?: (vaultDir: string, config: VaultConfig) => Promise<DoctorResult>;
29
- checkHarnessBinaryFn?: (config: VaultConfig) => Promise<DoctorResult>;
30
- checkQmdEmbeddingsFn?: (config: VaultConfig) => Promise<DoctorResult>;
31
- checkVersionDriftFn?: (
32
- vaultDir: string,
33
- config: VaultConfig,
34
- binaryVersion?: string,
35
- ) => Promise<DoctorResult>;
36
- checkOrphanCheckpointsFn?: (vaultDir: string, config: VaultConfig) => Promise<DoctorResult>;
37
- checkSandboxFn?: (config: VaultConfig) => DoctorResult | Promise<DoctorResult>;
38
- }
39
-
40
- export interface DoctorCommandResult {
41
- ok: boolean;
42
- exitCode: number;
43
- errorCount: number;
44
- warningCount: number;
45
- }
46
-
47
- // ---------------------------------------------------------------------------
48
- // Main runDoctor (pure, testable)
49
- // ---------------------------------------------------------------------------
50
-
51
- export async function runDoctor(opts: DoctorOptions = {}): Promise<DoctorCommandResult> {
52
- const vaultDir = opts.vaultDir ?? process.cwd();
53
- const isTTY = opts.isTTY ?? process.stdout.isTTY ?? false;
54
- const binaryVersion = opts.binaryVersion;
55
-
56
- const checkVaultYmlFn = opts.checkVaultYmlFn ?? checkVaultYml;
57
- const loadVaultConfigFn = opts.loadVaultConfigFn ?? loadVaultConfig;
58
- const checkFoldersFn = opts.checkFoldersFn ?? checkFolders;
59
- const checkHarnessBinaryFn = opts.checkHarnessBinaryFn ?? checkHarnessBinary;
60
- const checkQmdEmbeddingsFn = opts.checkQmdEmbeddingsFn ?? checkQmdEmbeddings;
61
- const checkVersionDriftFn = opts.checkVersionDriftFn ?? checkVersionDrift;
62
- const checkOrphanCheckpointsFn = opts.checkOrphanCheckpointsFn ?? checkOrphanCheckpoints;
63
- const checkSandboxFn = opts.checkSandboxFn ?? checkSandbox;
64
-
65
- const vaultYmlResult = await checkVaultYmlFn(vaultDir);
66
-
67
- let config: VaultConfig = {
68
- folders: {
69
- inbox: '00-inbox',
70
- projects: '01-projects',
71
- areas: '02-areas',
72
- knowledge: '03-knowledge',
73
- resources: '04-resources',
74
- agent: '05-agent',
75
- archive: '06-archive',
76
- logs: '07-logs',
77
- },
78
- };
79
-
80
- if (vaultYmlResult.status === 'ok') {
81
- try {
82
- config = await loadVaultConfigFn(vaultDir);
83
- } catch {
84
- // If loading fails, use default config above
85
- }
86
- }
87
-
88
- const [
89
- foldersResult,
90
- harnessResult,
91
- qmdResult,
92
- versionDriftResult,
93
- orphanCheckpointsResult,
94
- sandboxResult,
95
- ] = await Promise.all([
96
- checkFoldersFn(vaultDir, config),
97
- checkHarnessBinaryFn(config),
98
- checkQmdEmbeddingsFn(config),
99
- checkVersionDriftFn(vaultDir, config, binaryVersion),
100
- checkOrphanCheckpointsFn(vaultDir, config),
101
- checkSandboxFn(config),
102
- ]);
103
-
104
- const results = [
105
- vaultYmlResult,
106
- foldersResult,
107
- harnessResult,
108
- qmdResult,
109
- versionDriftResult,
110
- orphanCheckpointsResult,
111
- sandboxResult,
112
- ];
113
-
114
- const errorCount = results.filter((r) => r.status === 'error').length;
115
- const warningCount = results.filter((r) => r.status === 'warn').length;
116
-
117
- printDoctorOutput(results, isTTY, errorCount, warningCount);
118
-
119
- return {
120
- ok: errorCount === 0,
121
- exitCode: errorCount > 0 ? 1 : 0,
122
- errorCount,
123
- warningCount,
124
- };
125
- }
126
-
127
- // ---------------------------------------------------------------------------
128
- // CLI entry point — thin wrapper, calls process.exit
129
- // ---------------------------------------------------------------------------
130
-
131
- export async function doctorCommand(opts: DoctorOptions = {}): Promise<void> {
132
- const result = await runDoctor(opts);
133
- process.exit(result.exitCode);
134
- }
135
-
136
- // ---------------------------------------------------------------------------
137
- // Formatting
138
- // ---------------------------------------------------------------------------
139
-
140
- function printDoctorOutput(
141
- results: DoctorResult[],
142
- isTTY: boolean,
143
- errorCount: number,
144
- warningCount: number,
145
- ): void {
146
- const lines: string[] = [];
147
-
148
- if (isTTY) {
149
- lines.push('');
150
- lines.push(' OneBrain Doctor 🔍');
151
- lines.push('');
152
- } else {
153
- lines.push('OneBrain Doctor 🔍');
154
- lines.push('');
155
- }
156
-
157
- for (const result of results) {
158
- const statusIcon = getStatusIcon(result.status);
159
- lines.push(formatCheckLine(result, statusIcon));
160
- if (result.hint) {
161
- lines.push(formatHintLine(result.hint));
162
- }
163
- }
164
-
165
- lines.push('');
166
-
167
- if (errorCount > 0 && warningCount > 0) {
168
- lines.push(`Summary: ${errorCount} errors, ${warningCount} warnings`);
169
- } else if (errorCount > 0) {
170
- lines.push(`Summary: ${errorCount} errors`);
171
- } else if (warningCount > 0) {
172
- lines.push(`Summary: ${warningCount} warnings — ok to run`);
173
- } else {
174
- lines.push('Summary: All checks passed');
175
- }
176
-
177
- if (isTTY) {
178
- lines.push('');
179
- }
180
-
181
- console.log(lines.join('\n'));
182
- }
183
-
184
- function getStatusIcon(status: 'ok' | 'warn' | 'error'): string {
185
- switch (status) {
186
- case 'ok':
187
- return '[✓]';
188
- case 'warn':
189
- return '[!]';
190
- case 'error':
191
- return '[✗]';
192
- default:
193
- return '[?]';
194
- }
195
- }
196
-
197
- function formatCheckLine(result: DoctorResult, icon: string): string {
198
- return ` ${icon} ${result.check.padEnd(20)} ${result.message}`;
199
- }
200
-
201
- function formatHintLine(hint: string): string {
202
- return ` → ${hint}`;
203
- }