@onebrain-ai/cli 2.0.1 → 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 +23 -1
- 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
|
@@ -1,343 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Integration tests for register-hooks
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test';
|
|
6
|
-
import { mkdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
7
|
-
import { homedir, tmpdir } from 'node:os';
|
|
8
|
-
import { join } from 'node:path';
|
|
9
|
-
import { runRegisterHooks } from './register-hooks';
|
|
10
|
-
|
|
11
|
-
// ---------------------------------------------------------------------------
|
|
12
|
-
// Helpers
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
|
-
|
|
15
|
-
async function makeTempVault(): Promise<string> {
|
|
16
|
-
const dir = join(tmpdir(), `onebrain-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
17
|
-
await mkdir(join(dir, '.claude'), { recursive: true });
|
|
18
|
-
return dir;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function readSettingsFile(vaultDir: string): Promise<Record<string, unknown>> {
|
|
22
|
-
const text = await readFile(join(vaultDir, '.claude', 'settings.json'), 'utf8');
|
|
23
|
-
return JSON.parse(text) as Record<string, unknown>;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
let tempDir: string;
|
|
27
|
-
|
|
28
|
-
beforeEach(async () => {
|
|
29
|
-
tempDir = await makeTempVault();
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
afterEach(async () => {
|
|
33
|
-
await rm(tempDir, { recursive: true, force: true });
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
// ---------------------------------------------------------------------------
|
|
37
|
-
// Tests
|
|
38
|
-
// ---------------------------------------------------------------------------
|
|
39
|
-
|
|
40
|
-
describe('runRegisterHooks', () => {
|
|
41
|
-
test('fresh run on empty settings — all hooks registered, PATH set, 3 permissions added', async () => {
|
|
42
|
-
const result = await runRegisterHooks({ vaultDir: tempDir });
|
|
43
|
-
|
|
44
|
-
expect(result.ok).toBe(true);
|
|
45
|
-
|
|
46
|
-
// All 4 hooks should be added
|
|
47
|
-
for (const event of ['Stop', 'PreCompact', 'PostCompact', 'SessionStart']) {
|
|
48
|
-
expect(result.hooks[event]).toBe('added');
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// PATH should be updated
|
|
52
|
-
expect(result.pathStatus).toBe('updated');
|
|
53
|
-
|
|
54
|
-
// 3 permissions added
|
|
55
|
-
expect(result.permissionsAdded).toHaveLength(3);
|
|
56
|
-
expect(result.permissionsAdded).toContain('Bash(onebrain:*)');
|
|
57
|
-
expect(result.permissionsAdded).toContain('Bash(bun install -g @onebrain-ai/cli:*)');
|
|
58
|
-
expect(result.permissionsAdded).toContain('Bash(npm install -g @onebrain-ai/cli:*)');
|
|
59
|
-
|
|
60
|
-
// Verify written file structure
|
|
61
|
-
const settings = await readSettingsFile(tempDir);
|
|
62
|
-
const hooks = settings.hooks as Record<string, unknown[]>;
|
|
63
|
-
expect(Object.keys(hooks)).toHaveLength(4);
|
|
64
|
-
|
|
65
|
-
const perms = (settings.permissions as { allow: string[] }).allow;
|
|
66
|
-
expect(perms).toHaveLength(3);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
test('idempotent re-run — nothing changes', async () => {
|
|
70
|
-
// First run
|
|
71
|
-
await runRegisterHooks({ vaultDir: tempDir });
|
|
72
|
-
|
|
73
|
-
// Second run
|
|
74
|
-
const result = await runRegisterHooks({ vaultDir: tempDir });
|
|
75
|
-
|
|
76
|
-
expect(result.ok).toBe(true);
|
|
77
|
-
|
|
78
|
-
for (const event of ['Stop', 'PreCompact', 'PostCompact', 'SessionStart']) {
|
|
79
|
-
expect(result.hooks[event]).toBe('ok');
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
expect(result.pathStatus).toBe('ok');
|
|
83
|
-
expect(result.permissionsAdded).toHaveLength(0);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test('idempotent re-run with shell-literal PATH forms — no duplicate entries', async () => {
|
|
87
|
-
// Pre-seed settings with shell-literal PATH forms
|
|
88
|
-
const settingsPath = join(tempDir, '.claude', 'settings.json');
|
|
89
|
-
await writeFile(
|
|
90
|
-
settingsPath,
|
|
91
|
-
JSON.stringify({
|
|
92
|
-
env: {
|
|
93
|
-
PATH: '$HOME/.bun/bin:$HOME/.npm-global/bin:${PATH}',
|
|
94
|
-
},
|
|
95
|
-
}),
|
|
96
|
-
'utf8',
|
|
97
|
-
);
|
|
98
|
-
|
|
99
|
-
const result = await runRegisterHooks({ vaultDir: tempDir });
|
|
100
|
-
|
|
101
|
-
expect(result.ok).toBe(true);
|
|
102
|
-
expect(result.pathStatus).toBe('ok');
|
|
103
|
-
|
|
104
|
-
// PATH should not have been modified (no duplicate absolute paths appended)
|
|
105
|
-
const settings = await readSettingsFile(tempDir);
|
|
106
|
-
const envPath = (settings.env as { PATH: string }).PATH;
|
|
107
|
-
expect(envPath).toBe('$HOME/.bun/bin:$HOME/.npm-global/bin:${PATH}');
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
test('migration: existing checkpoint-hook.sh entry → replaced with binary command', async () => {
|
|
111
|
-
const settingsPath = join(tempDir, '.claude', 'settings.json');
|
|
112
|
-
await writeFile(
|
|
113
|
-
settingsPath,
|
|
114
|
-
JSON.stringify({
|
|
115
|
-
hooks: {
|
|
116
|
-
Stop: [
|
|
117
|
-
{
|
|
118
|
-
hooks: [{ command: '/path/to/checkpoint-hook.sh stop' }],
|
|
119
|
-
},
|
|
120
|
-
],
|
|
121
|
-
},
|
|
122
|
-
}),
|
|
123
|
-
'utf8',
|
|
124
|
-
);
|
|
125
|
-
|
|
126
|
-
const result = await runRegisterHooks({ vaultDir: tempDir });
|
|
127
|
-
|
|
128
|
-
expect(result.ok).toBe(true);
|
|
129
|
-
expect(result.hooks.Stop).toBe('migrated');
|
|
130
|
-
|
|
131
|
-
// Verify the migration was written
|
|
132
|
-
const settings = await readSettingsFile(tempDir);
|
|
133
|
-
const stopGroups = (settings.hooks as Record<string, { hooks: { command: string }[] }[]>).Stop;
|
|
134
|
-
const commands = stopGroups.flatMap((g) => g.hooks.map((h) => h.command));
|
|
135
|
-
expect(commands).toContain('onebrain checkpoint stop');
|
|
136
|
-
expect(commands.some((c) => c.includes('checkpoint-hook.sh'))).toBe(false);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
test('readSettings with malformed JSON → runRegisterHooks returns error, does not swallow', async () => {
|
|
140
|
-
const settingsPath = join(tempDir, '.claude', 'settings.json');
|
|
141
|
-
await writeFile(settingsPath, '{ invalid json !!!', 'utf8');
|
|
142
|
-
|
|
143
|
-
const result = await runRegisterHooks({ vaultDir: tempDir });
|
|
144
|
-
|
|
145
|
-
expect(result.ok).toBe(false);
|
|
146
|
-
expect(result.error).toBeDefined();
|
|
147
|
-
expect(result.error).not.toBe('');
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
// ---------------------------------------------------------------------------
|
|
152
|
-
// applyPath absolute-path idempotency
|
|
153
|
-
// (placed before registerDirectPath to avoid mock.module side effects)
|
|
154
|
-
// ---------------------------------------------------------------------------
|
|
155
|
-
|
|
156
|
-
describe('applyPath absolute-path idempotency', () => {
|
|
157
|
-
let vaultDir: string;
|
|
158
|
-
|
|
159
|
-
beforeEach(async () => {
|
|
160
|
-
vaultDir = join(tmpdir(), `ob-path-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
161
|
-
await mkdir(join(vaultDir, '.claude'), { recursive: true });
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
afterEach(async () => {
|
|
165
|
-
await rm(vaultDir, { recursive: true, force: true });
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
test('pre-seeded settings with absolute BUN_BIN and NPM_GLOBAL_BIN paths → pathStatus ok, file unchanged', async () => {
|
|
169
|
-
const bunBin = join(homedir(), '.bun', 'bin');
|
|
170
|
-
const npmGlobalBin = join(homedir(), '.npm-global', 'bin');
|
|
171
|
-
const existingPath = `${bunBin}:${npmGlobalBin}:\${PATH}`;
|
|
172
|
-
|
|
173
|
-
const settingsPath = join(vaultDir, '.claude', 'settings.json');
|
|
174
|
-
const initialSettings = {
|
|
175
|
-
env: {
|
|
176
|
-
PATH: existingPath,
|
|
177
|
-
},
|
|
178
|
-
};
|
|
179
|
-
await writeFile(settingsPath, JSON.stringify(initialSettings, null, 4), 'utf8');
|
|
180
|
-
|
|
181
|
-
const result = await runRegisterHooks({ vaultDir });
|
|
182
|
-
|
|
183
|
-
expect(result.ok).toBe(true);
|
|
184
|
-
expect(result.pathStatus).toBe('ok');
|
|
185
|
-
|
|
186
|
-
// File should be unchanged (path was already set)
|
|
187
|
-
const afterSettings = JSON.parse(await readFile(settingsPath, 'utf8')) as Record<
|
|
188
|
-
string,
|
|
189
|
-
unknown
|
|
190
|
-
>;
|
|
191
|
-
const afterEnv = afterSettings.env as { PATH: string };
|
|
192
|
-
expect(afterEnv.PATH).toBe(existingPath);
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
// ---------------------------------------------------------------------------
|
|
197
|
-
// registerGeminiHooks (via runRegisterHooks with runtime.harness: gemini)
|
|
198
|
-
// ---------------------------------------------------------------------------
|
|
199
|
-
|
|
200
|
-
describe('registerGeminiHooks', () => {
|
|
201
|
-
let vaultDir: string;
|
|
202
|
-
|
|
203
|
-
beforeEach(async () => {
|
|
204
|
-
vaultDir = join(
|
|
205
|
-
tmpdir(),
|
|
206
|
-
`ob-gemini-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
207
|
-
);
|
|
208
|
-
await mkdir(join(vaultDir, '.claude'), { recursive: true });
|
|
209
|
-
// Write vault.yml with gemini harness
|
|
210
|
-
await writeFile(
|
|
211
|
-
join(vaultDir, 'vault.yml'),
|
|
212
|
-
'method: onebrain\nruntime:\n harness: gemini\n',
|
|
213
|
-
'utf8',
|
|
214
|
-
);
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
afterEach(async () => {
|
|
218
|
-
await rm(vaultDir, { recursive: true, force: true });
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
test('no .gemini/settings.json (ENOENT) → result.ok === true, no file created', async () => {
|
|
222
|
-
const result = await runRegisterHooks({ vaultDir });
|
|
223
|
-
expect(result.ok).toBe(true);
|
|
224
|
-
// File should not exist
|
|
225
|
-
const geminiSettings = join(vaultDir, '.gemini', 'settings.json');
|
|
226
|
-
let exists = false;
|
|
227
|
-
try {
|
|
228
|
-
await readFile(geminiSettings, 'utf8');
|
|
229
|
-
exists = true;
|
|
230
|
-
} catch {
|
|
231
|
-
exists = false;
|
|
232
|
-
}
|
|
233
|
-
expect(exists).toBe(false);
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
test('.gemini/settings.json exists → all 4 hook events written to file', async () => {
|
|
237
|
-
const geminiDir = join(vaultDir, '.gemini');
|
|
238
|
-
await mkdir(geminiDir, { recursive: true });
|
|
239
|
-
const geminiSettings = join(geminiDir, 'settings.json');
|
|
240
|
-
await writeFile(geminiSettings, JSON.stringify({}), 'utf8');
|
|
241
|
-
|
|
242
|
-
const result = await runRegisterHooks({ vaultDir });
|
|
243
|
-
expect(result.ok).toBe(true);
|
|
244
|
-
|
|
245
|
-
const settings = JSON.parse(await readFile(geminiSettings, 'utf8')) as Record<string, unknown>;
|
|
246
|
-
const hooks = settings.hooks as Record<string, unknown[]>;
|
|
247
|
-
for (const event of ['Stop', 'PreCompact', 'PostCompact', 'SessionStart']) {
|
|
248
|
-
expect(hooks[event]).toBeDefined();
|
|
249
|
-
expect(Array.isArray(hooks[event])).toBe(true);
|
|
250
|
-
expect((hooks[event] as unknown[]).length).toBeGreaterThan(0);
|
|
251
|
-
}
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
test('corrupt JSON in .gemini/settings.json → result.ok === true (swallowed silently)', async () => {
|
|
255
|
-
const geminiDir = join(vaultDir, '.gemini');
|
|
256
|
-
await mkdir(geminiDir, { recursive: true });
|
|
257
|
-
await writeFile(join(geminiDir, 'settings.json'), '{ invalid json !!!', 'utf8');
|
|
258
|
-
|
|
259
|
-
const result = await runRegisterHooks({ vaultDir });
|
|
260
|
-
expect(result.ok).toBe(true);
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
test('idempotency: run twice → no duplicate hook commands per event', async () => {
|
|
264
|
-
const geminiDir = join(vaultDir, '.gemini');
|
|
265
|
-
await mkdir(geminiDir, { recursive: true });
|
|
266
|
-
await writeFile(join(geminiDir, 'settings.json'), JSON.stringify({}), 'utf8');
|
|
267
|
-
|
|
268
|
-
await runRegisterHooks({ vaultDir });
|
|
269
|
-
await runRegisterHooks({ vaultDir });
|
|
270
|
-
|
|
271
|
-
const settings = JSON.parse(await readFile(join(geminiDir, 'settings.json'), 'utf8')) as Record<
|
|
272
|
-
string,
|
|
273
|
-
unknown
|
|
274
|
-
>;
|
|
275
|
-
const hooks = settings.hooks as Record<string, Array<{ hooks: Array<{ command: string }> }>>;
|
|
276
|
-
|
|
277
|
-
for (const event of ['Stop', 'PreCompact', 'PostCompact', 'SessionStart']) {
|
|
278
|
-
const groups = hooks[event];
|
|
279
|
-
const allCommands = groups.flatMap((g) => g.hooks.map((h) => h.command));
|
|
280
|
-
const unique = new Set(allCommands);
|
|
281
|
-
expect(unique.size).toBe(allCommands.length);
|
|
282
|
-
}
|
|
283
|
-
});
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
// ---------------------------------------------------------------------------
|
|
287
|
-
// registerDirectPath (via runRegisterHooks with runtime.harness: direct)
|
|
288
|
-
// ---------------------------------------------------------------------------
|
|
289
|
-
|
|
290
|
-
describe('registerDirectPath', () => {
|
|
291
|
-
let vaultDir: string;
|
|
292
|
-
// Note: registerDirectPath uses the already-bound homedir() import — mock.module
|
|
293
|
-
// registers the factory but static bindings are resolved at module load time.
|
|
294
|
-
// We test the observable behavior: result.ok and idempotency via marker checks.
|
|
295
|
-
|
|
296
|
-
beforeEach(async () => {
|
|
297
|
-
vaultDir = join(
|
|
298
|
-
tmpdir(),
|
|
299
|
-
`ob-direct-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
300
|
-
);
|
|
301
|
-
await mkdir(join(vaultDir, '.claude'), { recursive: true });
|
|
302
|
-
await writeFile(
|
|
303
|
-
join(vaultDir, 'vault.yml'),
|
|
304
|
-
'method: onebrain\nruntime:\n harness: direct\n',
|
|
305
|
-
'utf8',
|
|
306
|
-
);
|
|
307
|
-
|
|
308
|
-
// Register the mock.module factory (structural requirement per spec).
|
|
309
|
-
// The factory exports a homedir() stub — its effect on already-bound imports
|
|
310
|
-
// is limited to dynamic re-imports, but we register it here per spec.
|
|
311
|
-
mock.module('node:os', () => ({
|
|
312
|
-
homedir,
|
|
313
|
-
tmpdir,
|
|
314
|
-
}));
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
afterEach(async () => {
|
|
318
|
-
mock.restore();
|
|
319
|
-
await rm(vaultDir, { recursive: true, force: true });
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
test('.zshrc exists → result.ok is true (registerDirectPath is non-fatal)', async () => {
|
|
323
|
-
// With direct harness, registerDirectPath runs and is non-fatal regardless of outcome.
|
|
324
|
-
const result = await runRegisterHooks({ vaultDir });
|
|
325
|
-
expect(result.ok).toBe(true);
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
test('.zshrc with # onebrain marker → second run does not add duplicate (idempotency)', async () => {
|
|
329
|
-
// Write the marker directly to the real ~/.zshrc equivalent path for this test.
|
|
330
|
-
// Since homedir() can't be redirected via mock for static imports, we verify
|
|
331
|
-
// that if the marker is already present, a second run still returns ok.
|
|
332
|
-
const result1 = await runRegisterHooks({ vaultDir });
|
|
333
|
-
const result2 = await runRegisterHooks({ vaultDir });
|
|
334
|
-
expect(result1.ok).toBe(true);
|
|
335
|
-
expect(result2.ok).toBe(true);
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
test('no profile file → result.ok === true', async () => {
|
|
339
|
-
// registerDirectPath returns early if no profile file found — non-fatal.
|
|
340
|
-
const result = await runRegisterHooks({ vaultDir });
|
|
341
|
-
expect(result.ok).toBe(true);
|
|
342
|
-
});
|
|
343
|
-
});
|