@onebrain-ai/cli 2.0.1 → 2.0.3

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.
@@ -1,477 +0,0 @@
1
- /**
2
- * init — Initialize a new OneBrain vault
3
- *
4
- * Steps:
5
- * 1. Detect existing vault.yml (--force, non-TTY exit-1, TTY prompt)
6
- * 2. Create standard folders (8 + inbox/imports)
7
- * 3. Write vault.yml (with harness auto-detect)
8
- * 4. Download plugin files (skip if .claude/plugins/onebrain/.claude-plugin/plugin.json exists)
9
- * 5. Register plugin (skip if source:marketplace entry exists)
10
- * 6. Run register-hooks
11
- *
12
- * TTY: uses @clack/prompts layout
13
- * Non-TTY: plain text lines
14
- *
15
- * Exit code: 0 on success, 1 on failure.
16
- */
17
-
18
- import { mkdir, readFile, rename, stat, writeFile } from 'node:fs/promises';
19
- import { homedir } from 'node:os';
20
- import { dirname, join } from 'node:path';
21
- import { confirm, intro, log, outro } from '@clack/prompts';
22
- import { stringify as stringifyYaml } from 'yaml';
23
-
24
- // ---------------------------------------------------------------------------
25
- // BUILD_VERSION shim
26
- // ---------------------------------------------------------------------------
27
-
28
- declare const BUILD_VERSION: string;
29
- const binaryVersion = typeof BUILD_VERSION !== 'undefined' ? BUILD_VERSION : 'dev';
30
-
31
- // ---------------------------------------------------------------------------
32
- // Types
33
- // ---------------------------------------------------------------------------
34
-
35
- export interface InitOptions {
36
- /** Vault root directory (default: process.cwd()). */
37
- vaultDir?: string;
38
- /** Harness override. */
39
- harness?: 'claude-code' | 'gemini' | 'direct';
40
- /** Overwrite existing vault.yml without prompting. */
41
- force?: boolean;
42
- /** Whether stdout is a TTY (default: process.stdout.isTTY). */
43
- isTTY?: boolean;
44
- /** Override path to installed_plugins.json (for tests). */
45
- installedPluginsPath?: string;
46
- /** Injectable vault-sync function (for tests). */
47
- vaultSyncFn?: (vaultDir: string, opts: Record<string, unknown>) => Promise<void>;
48
- /** Injectable register-hooks function (for tests). */
49
- registerHooksFn?: (vaultDir: string) => Promise<void>;
50
- }
51
-
52
- export interface InitResult {
53
- ok: boolean;
54
- exitCode: number;
55
- /** Human-readable message (used for non-TTY output / test assertions). */
56
- message?: string;
57
- foldersCreated: number;
58
- harness: string;
59
- pluginSkipped: boolean;
60
- pluginRegistrationSkipped: boolean;
61
- }
62
-
63
- // ---------------------------------------------------------------------------
64
- // Standard vault folders
65
- // ---------------------------------------------------------------------------
66
-
67
- /** [folder-path, create-parent-only] */
68
- const STANDARD_FOLDERS: string[] = [
69
- '00-inbox',
70
- '01-projects',
71
- '02-areas',
72
- '03-knowledge',
73
- '04-resources',
74
- '05-agent',
75
- '06-archive',
76
- '07-logs',
77
- ];
78
-
79
- // inbox/imports is a sub-directory that must also be created
80
- const INBOX_IMPORTS = join('00-inbox', 'imports');
81
-
82
- // ---------------------------------------------------------------------------
83
- // Helpers
84
- // ---------------------------------------------------------------------------
85
-
86
- async function pathExists(p: string): Promise<boolean> {
87
- try {
88
- await stat(p);
89
- return true;
90
- } catch {
91
- return false;
92
- }
93
- }
94
-
95
- /**
96
- * Auto-detect harness from environment and vault layout.
97
- * Priority: CLAUDE_CODE_HARNESS env → .claude/ directory → 'direct'
98
- */
99
- async function detectHarness(vaultDir: string): Promise<string> {
100
- const envHarness = process.env.CLAUDE_CODE_HARNESS;
101
- if (envHarness) return envHarness;
102
-
103
- if (await pathExists(join(vaultDir, '.claude'))) return 'claude-code';
104
-
105
- return 'direct';
106
- }
107
-
108
- // ---------------------------------------------------------------------------
109
- // Steps
110
- // ---------------------------------------------------------------------------
111
-
112
- async function createFolders(vaultDir: string): Promise<number> {
113
- let created = 0;
114
-
115
- const allPaths = [...STANDARD_FOLDERS, INBOX_IMPORTS];
116
-
117
- for (const rel of allPaths) {
118
- const full = join(vaultDir, rel);
119
- if (!(await pathExists(full))) {
120
- await mkdir(full, { recursive: true });
121
- created++;
122
- }
123
- }
124
-
125
- return created;
126
- }
127
-
128
- const VAULT_YML_DEFAULTS = {
129
- method: 'onebrain',
130
- update_channel: 'stable',
131
- folders: {
132
- inbox: '00-inbox',
133
- projects: '01-projects',
134
- areas: '02-areas',
135
- knowledge: '03-knowledge',
136
- resources: '04-resources',
137
- agent: '05-agent',
138
- archive: '06-archive',
139
- logs: '07-logs',
140
- },
141
- checkpoint: {
142
- messages: 15,
143
- minutes: 30,
144
- },
145
- runtime: {
146
- harness: 'claude-code',
147
- },
148
- };
149
-
150
- async function writeVaultYml(vaultDir: string, harness: string): Promise<void> {
151
- const config = {
152
- ...VAULT_YML_DEFAULTS,
153
- runtime: { harness },
154
- };
155
- const content = stringifyYaml(config, { lineWidth: 0 });
156
- await writeFile(join(vaultDir, 'vault.yml'), content, 'utf8');
157
- }
158
-
159
- /**
160
- * Step 4: Download plugin files (skip if already present).
161
- * Returns { skipped, driftWarning }.
162
- */
163
- async function downloadPluginFiles(
164
- vaultDir: string,
165
- vaultSyncFn: (vaultDir: string, opts: Record<string, unknown>) => Promise<void>,
166
- ): Promise<{ skipped: boolean; driftWarning?: string; failed?: boolean }> {
167
- const pluginJsonPath = join(
168
- vaultDir,
169
- '.claude',
170
- 'plugins',
171
- 'onebrain',
172
- '.claude-plugin',
173
- 'plugin.json',
174
- );
175
-
176
- if (await pathExists(pluginJsonPath)) {
177
- // Check version drift
178
- let pluginVersion: string | undefined;
179
- try {
180
- const text = await readFile(pluginJsonPath, 'utf8');
181
- const parsed = JSON.parse(text) as Record<string, unknown>;
182
- pluginVersion = typeof parsed.version === 'string' ? parsed.version : undefined;
183
- } catch {
184
- // Non-fatal
185
- }
186
-
187
- let driftWarning: string | undefined;
188
- if (pluginVersion && binaryVersion !== 'dev' && pluginVersion !== binaryVersion) {
189
- driftWarning = `Plugin files v${pluginVersion}, binary v${binaryVersion} — run onebrain update to sync.`;
190
- }
191
-
192
- return { skipped: true, driftWarning };
193
- }
194
-
195
- // Plugin files not present — run vault-sync (non-fatal)
196
- try {
197
- await vaultSyncFn(vaultDir, {});
198
- } catch (err) {
199
- const msg = err instanceof Error ? err.message : String(err);
200
- process.stderr.write(`init: vault-sync warning: ${msg}\n`);
201
- return { skipped: false, failed: true };
202
- }
203
-
204
- return { skipped: false };
205
- }
206
-
207
- /**
208
- * Step 5: Register plugin in installed_plugins.json.
209
- * Skips if a source:marketplace entry already exists.
210
- * Returns { skipped }.
211
- */
212
- async function registerPlugin(
213
- vaultDir: string,
214
- installedPluginsPath: string,
215
- ): Promise<{ skipped: boolean }> {
216
- // Read existing file
217
- let data: Record<string, unknown>;
218
- try {
219
- const text = await readFile(installedPluginsPath, 'utf8');
220
- data = JSON.parse(text) as Record<string, unknown>;
221
- } catch {
222
- data = { plugins: {} };
223
- }
224
-
225
- const plugins = (data.plugins ?? {}) as Record<string, unknown[]>;
226
- data.plugins = plugins;
227
-
228
- // Check if any onebrain@ key has a marketplace entry
229
- const hasMarketplace = Object.keys(plugins)
230
- .filter((k) => k.startsWith('onebrain@'))
231
- .some((k) => {
232
- const entries = plugins[k] as Array<Record<string, unknown>>;
233
- return entries.some((e) => e.source === 'marketplace');
234
- });
235
-
236
- if (hasMarketplace) {
237
- return { skipped: true };
238
- }
239
-
240
- // Read plugin version from .claude-plugin/plugin.json or plugin.json
241
- let pluginVersion = '0.0.0';
242
- const candidatePaths = [
243
- join(vaultDir, '.claude', 'plugins', 'onebrain', '.claude-plugin', 'plugin.json'),
244
- join(vaultDir, '.claude', 'plugins', 'onebrain', 'plugin.json'),
245
- ];
246
- for (const p of candidatePaths) {
247
- try {
248
- const text = await readFile(p, 'utf8');
249
- const parsed = JSON.parse(text) as Record<string, unknown>;
250
- if (typeof parsed.version === 'string') {
251
- pluginVersion = parsed.version;
252
- break;
253
- }
254
- } catch {
255
- // Try next
256
- }
257
- }
258
-
259
- const installPath = join(vaultDir, '.claude', 'plugins', 'onebrain');
260
- const key = `onebrain@${pluginVersion}`;
261
-
262
- // Upsert entry
263
- if (!plugins[key]) {
264
- plugins[key] = [];
265
- }
266
- const entries = plugins[key] as Array<Record<string, unknown>>;
267
- const existingIdx = entries.findIndex((e) => e.source !== 'marketplace');
268
-
269
- if (existingIdx >= 0) {
270
- entries[existingIdx].installPath = installPath;
271
- entries[existingIdx].version = pluginVersion;
272
- } else {
273
- entries.push({ source: 'local', installPath, version: pluginVersion });
274
- }
275
-
276
- // Write atomically
277
- const tmpPath = `${installedPluginsPath}.tmp`;
278
- try {
279
- await mkdir(dirname(installedPluginsPath), { recursive: true });
280
- await writeFile(tmpPath, JSON.stringify(data, null, 4), 'utf8');
281
- await rename(tmpPath, installedPluginsPath);
282
- } catch (err) {
283
- const msg = err instanceof Error ? err.message : String(err);
284
- process.stderr.write(`init: plugin registration warning: ${msg}\n`);
285
- return { skipped: false };
286
- }
287
-
288
- return { skipped: false };
289
- }
290
-
291
- // ---------------------------------------------------------------------------
292
- // Main runInit
293
- // ---------------------------------------------------------------------------
294
-
295
- export async function runInit(opts: InitOptions = {}): Promise<InitResult> {
296
- const vaultDir = opts.vaultDir ?? process.cwd();
297
- const isTTY = opts.isTTY ?? process.stdout.isTTY ?? false;
298
- const force = opts.force ?? false;
299
- const installedPluginsPath =
300
- opts.installedPluginsPath ?? join(homedir(), '.claude', 'plugins', 'installed_plugins.json');
301
-
302
- // Injectable dependencies (real implementations lazy-loaded)
303
- const vaultSyncFn =
304
- opts.vaultSyncFn ??
305
- (async (dir: string, syncOpts: Record<string, unknown>) => {
306
- const { vaultSyncCommand } = await import('../internal/vault-sync.js');
307
- await vaultSyncCommand(dir, syncOpts);
308
- });
309
-
310
- const registerHooksFn =
311
- opts.registerHooksFn ??
312
- (async (dir: string) => {
313
- const { runRegisterHooks } = await import('../internal/register-hooks.js');
314
- await runRegisterHooks({ vaultDir: dir });
315
- });
316
-
317
- const result: InitResult = {
318
- ok: false,
319
- exitCode: 0,
320
- foldersCreated: 0,
321
- harness: 'direct',
322
- pluginSkipped: false,
323
- pluginRegistrationSkipped: false,
324
- };
325
-
326
- // Output helpers
327
- function writeLine(msg: string) {
328
- process.stdout.write(`${msg}\n`);
329
- }
330
-
331
- function noteStep(step: string, detail: string) {
332
- if (isTTY) {
333
- log.step(`${step}\n│ ${detail}`);
334
- } else {
335
- writeLine(`${step}: ${detail}`);
336
- }
337
- }
338
-
339
- function noteInfo(msg: string) {
340
- if (isTTY) {
341
- log.info(msg);
342
- } else {
343
- writeLine(msg);
344
- }
345
- }
346
-
347
- // ── Step 1: Detect existing vault.yml ─────────────────────────────────────
348
-
349
- const vaultYmlPath = join(vaultDir, 'vault.yml');
350
- const vaultYmlExists = await pathExists(vaultYmlPath);
351
-
352
- if (vaultYmlExists && !force) {
353
- if (!isTTY) {
354
- const msg = 'vault.yml exists. Re-run with --force to overwrite.';
355
- process.stdout.write(`${msg}\n`);
356
- result.message = msg;
357
- result.exitCode = 1;
358
- return result;
359
- }
360
-
361
- // TTY: prompt user
362
- if (isTTY) {
363
- intro('OneBrain Init');
364
- const overwrite = await confirm({
365
- message: 'vault.yml already exists. Overwrite?',
366
- });
367
- if (!overwrite || overwrite === Symbol.for('clack:cancel')) {
368
- result.ok = true;
369
- result.exitCode = 0;
370
- return result;
371
- }
372
- }
373
- } else if (isTTY && !vaultYmlExists) {
374
- intro('OneBrain Init');
375
- log.message('');
376
- } else if (isTTY && force) {
377
- intro('OneBrain Init');
378
- log.message('');
379
- }
380
-
381
- // Non-TTY header (TTY uses intro() above)
382
- if (!isTTY) {
383
- writeLine('OneBrain Init');
384
- }
385
-
386
- // ── Step 2: Create standard folders ───────────────────────────────────────
387
-
388
- const foldersCreated = await createFolders(vaultDir);
389
- result.foldersCreated = foldersCreated;
390
- noteStep(
391
- 'Creating vault structure',
392
- `${foldersCreated} folder${foldersCreated !== 1 ? 's' : ''} created`,
393
- );
394
-
395
- // ── Step 3: Write vault.yml ────────────────────────────────────────────────
396
-
397
- const harness = opts.harness ?? (await detectHarness(vaultDir));
398
- result.harness = harness;
399
- await writeVaultYml(vaultDir, harness);
400
- noteStep('Writing vault.yml', `harness: ${harness}`);
401
-
402
- // ── Step 4: Download plugin files ─────────────────────────────────────────
403
-
404
- const {
405
- skipped: pluginSkipped,
406
- driftWarning,
407
- failed: pluginDownloadFailed,
408
- } = await downloadPluginFiles(vaultDir, vaultSyncFn);
409
- result.pluginSkipped = pluginSkipped;
410
-
411
- if (pluginDownloadFailed) {
412
- noteInfo('vault-sync failed — run onebrain update to download plugin files');
413
- } else if (driftWarning) {
414
- noteInfo(driftWarning);
415
- }
416
-
417
- // ── Step 5: Register plugin ────────────────────────────────────────────────
418
-
419
- const { skipped: pluginRegistrationSkipped } = await registerPlugin(
420
- vaultDir,
421
- installedPluginsPath,
422
- );
423
- result.pluginRegistrationSkipped = pluginRegistrationSkipped;
424
- noteStep(
425
- 'Registering plugin',
426
- `installed_plugins.json: ${pluginRegistrationSkipped ? 'skipped (marketplace)' : '✓'}`,
427
- );
428
-
429
- // ── Step 6: Register hooks ─────────────────────────────────────────────────
430
-
431
- let hooksOk = true;
432
- try {
433
- await registerHooksFn(vaultDir);
434
- } catch (err) {
435
- hooksOk = false;
436
- const msg = err instanceof Error ? err.message : String(err);
437
- process.stderr.write(`init: register-hooks warning: ${msg}\n`);
438
- }
439
-
440
- const hooksLine = hooksOk ? 'ok' : 'warning — hooks not registered; run onebrain update';
441
- if (isTTY) {
442
- log.step(`Registering hooks\n│ ${hooksLine}`);
443
- } else {
444
- writeLine(`hooks: ${hooksLine}`);
445
- }
446
-
447
- // ── Done ──────────────────────────────────────────────────────────────────
448
-
449
- result.ok = true;
450
- result.exitCode = 0;
451
-
452
- const doneMsg = 'run /onboarding in Claude to finish setup';
453
- if (isTTY) {
454
- outro(`Done — ${doneMsg}`);
455
- } else {
456
- writeLine(`done: ${doneMsg}`);
457
- }
458
-
459
- return result;
460
- }
461
-
462
- // ---------------------------------------------------------------------------
463
- // CLI entry point (called from index.ts)
464
- // ---------------------------------------------------------------------------
465
-
466
- export interface InitCommandOptions {
467
- vaultDir?: string;
468
- harness?: 'claude-code' | 'gemini' | 'direct';
469
- force?: boolean;
470
- }
471
-
472
- export async function initCommand(opts: InitCommandOptions = {}): Promise<void> {
473
- const result = await runInit(opts);
474
- if (!result.ok) {
475
- process.exit(result.exitCode || 1);
476
- }
477
- }