@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 +3 -3
- package/package.json +24 -2
- package/src/commands/doctor.test.ts +0 -416
- package/src/commands/doctor.ts +0 -203
- package/src/commands/init.test.ts +0 -318
- package/src/commands/init.ts +0 -477
- package/src/commands/update.test.ts +0 -413
- package/src/commands/update.ts +0 -353
- package/src/index.ts +0 -144
- package/src/internal/__snapshots__/checkpoint.test.ts.snap +0 -12
- package/src/internal/__snapshots__/orphan-scan.test.ts.snap +0 -13
- package/src/internal/__snapshots__/session-init.test.ts.snap +0 -15
- package/src/internal/checkpoint.test.ts +0 -741
- package/src/internal/checkpoint.ts +0 -427
- package/src/internal/migrate.test.ts +0 -301
- package/src/internal/migrate.ts +0 -186
- package/src/internal/orphan-scan.test.ts +0 -271
- package/src/internal/orphan-scan.ts +0 -213
- package/src/internal/qmd-reindex.test.ts +0 -117
- package/src/internal/qmd-reindex.ts +0 -44
- package/src/internal/register-hooks.test.ts +0 -343
- package/src/internal/register-hooks.ts +0 -418
- package/src/internal/session-init.test.ts +0 -318
- package/src/internal/session-init.ts +0 -264
- package/src/internal/vault-sync.test.ts +0 -419
- package/src/internal/vault-sync.ts +0 -764
- package/tests/integration/init.integration.test.ts +0 -304
- package/tests/integration/update.integration.test.ts +0 -306
- package/tsconfig.json +0 -12
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 =
|
|
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 =
|
|
12602
|
-
var RELEASE_DATE =
|
|
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.
|
|
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
|
-
});
|
package/src/commands/doctor.ts
DELETED
|
@@ -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
|
-
}
|