@onebrain-ai/cli 2.0.0
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 +12656 -0
- package/package.json +23 -0
- package/src/commands/doctor.test.ts +416 -0
- package/src/commands/doctor.ts +203 -0
- package/src/commands/init.test.ts +318 -0
- package/src/commands/init.ts +477 -0
- package/src/commands/update.test.ts +413 -0
- package/src/commands/update.ts +353 -0
- package/src/index.ts +144 -0
- package/src/internal/__snapshots__/checkpoint.test.ts.snap +12 -0
- package/src/internal/__snapshots__/orphan-scan.test.ts.snap +13 -0
- package/src/internal/__snapshots__/session-init.test.ts.snap +15 -0
- package/src/internal/checkpoint.test.ts +741 -0
- package/src/internal/checkpoint.ts +427 -0
- package/src/internal/migrate.test.ts +301 -0
- package/src/internal/migrate.ts +186 -0
- package/src/internal/orphan-scan.test.ts +271 -0
- package/src/internal/orphan-scan.ts +213 -0
- package/src/internal/qmd-reindex.test.ts +117 -0
- package/src/internal/qmd-reindex.ts +44 -0
- package/src/internal/register-hooks.test.ts +343 -0
- package/src/internal/register-hooks.ts +418 -0
- package/src/internal/session-init.test.ts +318 -0
- package/src/internal/session-init.ts +264 -0
- package/src/internal/vault-sync.test.ts +419 -0
- package/src/internal/vault-sync.ts +764 -0
- package/tests/integration/init.integration.test.ts +304 -0
- package/tests/integration/update.integration.test.ts +306 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for `onebrain init`
|
|
3
|
+
*
|
|
4
|
+
* Tests run against a temp vault dir. Process TTY is always false in test
|
|
5
|
+
* (piped stdout), so all non-TTY paths are exercised directly.
|
|
6
|
+
*
|
|
7
|
+
* Vault-sync and register-hooks are mocked so tests stay offline and fast.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
|
11
|
+
import { mkdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
|
|
15
|
+
import { type InitOptions, runInit } from './init.js';
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Helpers
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
async function makeTempVault(): Promise<string> {
|
|
22
|
+
const dir = join(
|
|
23
|
+
tmpdir(),
|
|
24
|
+
`onebrain-init-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
25
|
+
);
|
|
26
|
+
await mkdir(dir, { recursive: true });
|
|
27
|
+
return dir;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function fileExists(path: string): Promise<boolean> {
|
|
31
|
+
try {
|
|
32
|
+
await stat(path);
|
|
33
|
+
return true;
|
|
34
|
+
} catch {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function readVaultYml(vaultDir: string): Promise<Record<string, unknown>> {
|
|
40
|
+
const { parse } = await import('yaml');
|
|
41
|
+
const text = await readFile(join(vaultDir, 'vault.yml'), 'utf8');
|
|
42
|
+
return (parse(text) ?? {}) as Record<string, unknown>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Noop mocks — capture calls but do nothing
|
|
46
|
+
const noopVaultSync = async (_vaultDir: string, _opts: Record<string, unknown>) => {
|
|
47
|
+
// no-op
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const noopRegisterHooks = async (_vaultDir: string) => {
|
|
51
|
+
// no-op
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
let tempDir: string;
|
|
55
|
+
|
|
56
|
+
beforeEach(async () => {
|
|
57
|
+
tempDir = await makeTempVault();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterEach(async () => {
|
|
61
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
62
|
+
// biome-ignore lint/performance/noDelete: env cleanup requires delete to unset, not assign "undefined"
|
|
63
|
+
delete process.env.CLAUDE_CODE_HARNESS;
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// Tests
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
describe('runInit', () => {
|
|
71
|
+
it('fresh vault — creates all standard folders + vault.yml + calls vault-sync', async () => {
|
|
72
|
+
let vaultSyncCalled = false;
|
|
73
|
+
const mockVaultSync = async (_vaultDir: string, _opts: Record<string, unknown>) => {
|
|
74
|
+
vaultSyncCalled = true;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const opts: InitOptions = {
|
|
78
|
+
vaultDir: tempDir,
|
|
79
|
+
vaultSyncFn: mockVaultSync,
|
|
80
|
+
registerHooksFn: noopRegisterHooks,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const result = await runInit(opts);
|
|
84
|
+
|
|
85
|
+
expect(result.ok).toBe(true);
|
|
86
|
+
|
|
87
|
+
// All 8 standard folders should exist
|
|
88
|
+
const folders = [
|
|
89
|
+
'00-inbox',
|
|
90
|
+
'01-projects',
|
|
91
|
+
'02-areas',
|
|
92
|
+
'03-knowledge',
|
|
93
|
+
'04-resources',
|
|
94
|
+
'05-agent',
|
|
95
|
+
'06-archive',
|
|
96
|
+
'07-logs',
|
|
97
|
+
];
|
|
98
|
+
for (const folder of folders) {
|
|
99
|
+
expect(await fileExists(join(tempDir, folder))).toBe(true);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// imports subdirectory inside inbox
|
|
103
|
+
expect(await fileExists(join(tempDir, '00-inbox', 'imports'))).toBe(true);
|
|
104
|
+
|
|
105
|
+
// vault.yml written
|
|
106
|
+
expect(await fileExists(join(tempDir, 'vault.yml'))).toBe(true);
|
|
107
|
+
const vaultYml = await readVaultYml(tempDir);
|
|
108
|
+
expect(vaultYml.method).toBe('onebrain');
|
|
109
|
+
expect(vaultYml.update_channel).toBe('stable');
|
|
110
|
+
|
|
111
|
+
// folders count: 8 standard + inbox/imports = 9 total
|
|
112
|
+
expect(result.foldersCreated).toBe(9);
|
|
113
|
+
|
|
114
|
+
// vault-sync was called (plugin files not present)
|
|
115
|
+
expect(vaultSyncCalled).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('existing vault.yml in non-TTY → exit 1 with message', async () => {
|
|
119
|
+
await writeFile(join(tempDir, 'vault.yml'), 'method: onebrain\n', 'utf8');
|
|
120
|
+
|
|
121
|
+
const opts: InitOptions = {
|
|
122
|
+
vaultDir: tempDir,
|
|
123
|
+
isTTY: false,
|
|
124
|
+
vaultSyncFn: noopVaultSync,
|
|
125
|
+
registerHooksFn: noopRegisterHooks,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const result = await runInit(opts);
|
|
129
|
+
|
|
130
|
+
expect(result.ok).toBe(false);
|
|
131
|
+
expect(result.exitCode).toBe(1);
|
|
132
|
+
expect(result.message).toContain('vault.yml exists');
|
|
133
|
+
expect(result.message).toContain('--force');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('--force overwrites existing vault.yml', async () => {
|
|
137
|
+
await writeFile(join(tempDir, 'vault.yml'), 'method: legacy\n', 'utf8');
|
|
138
|
+
|
|
139
|
+
const opts: InitOptions = {
|
|
140
|
+
vaultDir: tempDir,
|
|
141
|
+
force: true,
|
|
142
|
+
vaultSyncFn: noopVaultSync,
|
|
143
|
+
registerHooksFn: noopRegisterHooks,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const result = await runInit(opts);
|
|
147
|
+
|
|
148
|
+
expect(result.ok).toBe(true);
|
|
149
|
+
const vaultYml = await readVaultYml(tempDir);
|
|
150
|
+
expect(vaultYml.method).toBe('onebrain');
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('plugin files already present — skips vault-sync download', async () => {
|
|
154
|
+
// Pre-create plugin.json to simulate existing plugin files
|
|
155
|
+
const pluginMetaDir = join(tempDir, '.claude', 'plugins', 'onebrain', '.claude-plugin');
|
|
156
|
+
await mkdir(pluginMetaDir, { recursive: true });
|
|
157
|
+
await writeFile(
|
|
158
|
+
join(pluginMetaDir, 'plugin.json'),
|
|
159
|
+
JSON.stringify({ version: '1.11.0' }),
|
|
160
|
+
'utf8',
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
let vaultSyncCalled = false;
|
|
164
|
+
const mockVaultSync = async (_vaultDir: string, _opts: Record<string, unknown>) => {
|
|
165
|
+
vaultSyncCalled = true;
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
const opts: InitOptions = {
|
|
169
|
+
vaultDir: tempDir,
|
|
170
|
+
vaultSyncFn: mockVaultSync,
|
|
171
|
+
registerHooksFn: noopRegisterHooks,
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const result = await runInit(opts);
|
|
175
|
+
|
|
176
|
+
expect(result.ok).toBe(true);
|
|
177
|
+
expect(vaultSyncCalled).toBe(false);
|
|
178
|
+
expect(result.pluginSkipped).toBe(true);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('source:marketplace in installed_plugins.json — skips plugin registration', async () => {
|
|
182
|
+
// Create installed_plugins.json with marketplace entry
|
|
183
|
+
const pluginsDir = join(tempDir, '.claude-meta');
|
|
184
|
+
await mkdir(pluginsDir, { recursive: true });
|
|
185
|
+
const installedPluginsPath = join(pluginsDir, 'installed_plugins.json');
|
|
186
|
+
await writeFile(
|
|
187
|
+
installedPluginsPath,
|
|
188
|
+
JSON.stringify({
|
|
189
|
+
plugins: {
|
|
190
|
+
'onebrain@1.0.0': [{ source: 'marketplace', installPath: '/some/path' }],
|
|
191
|
+
},
|
|
192
|
+
}),
|
|
193
|
+
'utf8',
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
const opts: InitOptions = {
|
|
197
|
+
vaultDir: tempDir,
|
|
198
|
+
vaultSyncFn: noopVaultSync,
|
|
199
|
+
registerHooksFn: noopRegisterHooks,
|
|
200
|
+
installedPluginsPath,
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const result = await runInit(opts);
|
|
204
|
+
|
|
205
|
+
expect(result.ok).toBe(true);
|
|
206
|
+
expect(result.pluginRegistrationSkipped).toBe(true);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('harness auto-detect: .claude/ dir present → claude-code', async () => {
|
|
210
|
+
await mkdir(join(tempDir, '.claude'), { recursive: true });
|
|
211
|
+
|
|
212
|
+
const opts: InitOptions = {
|
|
213
|
+
vaultDir: tempDir,
|
|
214
|
+
vaultSyncFn: noopVaultSync,
|
|
215
|
+
registerHooksFn: noopRegisterHooks,
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
const result = await runInit(opts);
|
|
219
|
+
|
|
220
|
+
expect(result.ok).toBe(true);
|
|
221
|
+
const vaultYml = await readVaultYml(tempDir);
|
|
222
|
+
const runtime = vaultYml.runtime as Record<string, unknown> | undefined;
|
|
223
|
+
expect(runtime?.harness).toBe('claude-code');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('harness auto-detect: no .claude/ dir → direct', async () => {
|
|
227
|
+
// Fresh vault, no .claude/ dir
|
|
228
|
+
const opts: InitOptions = {
|
|
229
|
+
vaultDir: tempDir,
|
|
230
|
+
vaultSyncFn: noopVaultSync,
|
|
231
|
+
registerHooksFn: noopRegisterHooks,
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const result = await runInit(opts);
|
|
235
|
+
|
|
236
|
+
expect(result.ok).toBe(true);
|
|
237
|
+
const vaultYml = await readVaultYml(tempDir);
|
|
238
|
+
const runtime = vaultYml.runtime as Record<string, unknown> | undefined;
|
|
239
|
+
expect(runtime?.harness).toBe('direct');
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it('--harness flag overrides auto-detect', async () => {
|
|
243
|
+
const opts: InitOptions = {
|
|
244
|
+
vaultDir: tempDir,
|
|
245
|
+
harness: 'gemini',
|
|
246
|
+
vaultSyncFn: noopVaultSync,
|
|
247
|
+
registerHooksFn: noopRegisterHooks,
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const result = await runInit(opts);
|
|
251
|
+
|
|
252
|
+
expect(result.ok).toBe(true);
|
|
253
|
+
const vaultYml = await readVaultYml(tempDir);
|
|
254
|
+
const runtime = vaultYml.runtime as Record<string, unknown> | undefined;
|
|
255
|
+
expect(runtime?.harness).toBe('gemini');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('existing folders not double-counted in foldersCreated', async () => {
|
|
259
|
+
// Pre-create some folders
|
|
260
|
+
await mkdir(join(tempDir, '00-inbox'), { recursive: true });
|
|
261
|
+
await mkdir(join(tempDir, '01-projects'), { recursive: true });
|
|
262
|
+
|
|
263
|
+
const opts: InitOptions = {
|
|
264
|
+
vaultDir: tempDir,
|
|
265
|
+
vaultSyncFn: noopVaultSync,
|
|
266
|
+
registerHooksFn: noopRegisterHooks,
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
const result = await runInit(opts);
|
|
270
|
+
|
|
271
|
+
expect(result.ok).toBe(true);
|
|
272
|
+
// 9 total (8 standard + imports), 2 already exist → 7 created
|
|
273
|
+
expect(result.foldersCreated).toBe(7);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('non-TTY output starts with OneBrain Init header', async () => {
|
|
277
|
+
const lines: string[] = [];
|
|
278
|
+
const originalWrite = process.stdout.write.bind(process.stdout);
|
|
279
|
+
process.stdout.write = (chunk: string | Uint8Array, ...args: unknown[]) => {
|
|
280
|
+
if (typeof chunk === 'string') lines.push(chunk);
|
|
281
|
+
return originalWrite(chunk, ...(args as Parameters<typeof originalWrite>).slice(1));
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
try {
|
|
285
|
+
const opts: InitOptions = {
|
|
286
|
+
vaultDir: tempDir,
|
|
287
|
+
isTTY: false,
|
|
288
|
+
vaultSyncFn: noopVaultSync,
|
|
289
|
+
registerHooksFn: noopRegisterHooks,
|
|
290
|
+
};
|
|
291
|
+
const result = await runInit(opts);
|
|
292
|
+
expect(result.ok).toBe(true);
|
|
293
|
+
} finally {
|
|
294
|
+
process.stdout.write = originalWrite;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const fullOutput = lines.join('');
|
|
298
|
+
expect(fullOutput).toMatch(/^OneBrain Init\n/);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('harness auto-detect: CLAUDE_CODE_HARNESS env → uses env value', async () => {
|
|
302
|
+
process.env.CLAUDE_CODE_HARNESS = 'gemini';
|
|
303
|
+
|
|
304
|
+
const opts: InitOptions = {
|
|
305
|
+
vaultDir: tempDir,
|
|
306
|
+
vaultSyncFn: noopVaultSync,
|
|
307
|
+
registerHooksFn: noopRegisterHooks,
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const result = await runInit(opts);
|
|
311
|
+
|
|
312
|
+
expect(result.ok).toBe(true);
|
|
313
|
+
expect(result.harness).toBe('gemini');
|
|
314
|
+
const vaultYml = await readVaultYml(tempDir);
|
|
315
|
+
const runtime = vaultYml.runtime as Record<string, unknown> | undefined;
|
|
316
|
+
expect(runtime?.harness).toBe('gemini');
|
|
317
|
+
});
|
|
318
|
+
});
|