@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,353 +0,0 @@
1
- /**
2
- * update — Atomic OneBrain update sequence
3
- *
4
- * Steps:
5
- * 1. Fetch latest release from GitHub (parse tag_name)
6
- * 2. Sync plugin files (vault-sync)
7
- * 3. (Handled by vault-sync Step 4 — merge harness files)
8
- * 4. Install binary (bun install -g / npm install -g on Windows)
9
- * 4b. Validate binary (ATOMIC GATE — register-hooks blocked if this fails)
10
- * 5. Register hooks (only if 4b passed)
11
- * 6. Write onebrain_version to vault.yml
12
- *
13
- * TTY: uses @clack/prompts layout
14
- * Non-TTY: plain text lines
15
- *
16
- * Exit code: 0 on success, 1 on failure.
17
- */
18
-
19
- import { readFile, rename, writeFile } from 'node:fs/promises';
20
- import { join } from 'node:path';
21
- import { intro, log, outro } from '@clack/prompts';
22
- import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
23
-
24
- // ---------------------------------------------------------------------------
25
- // Types
26
- // ---------------------------------------------------------------------------
27
-
28
- export interface UpdateOptions {
29
- /** Vault root directory (default: process.cwd()). */
30
- vaultDir?: string;
31
- /** Whether stdout is a TTY (default: process.stdout.isTTY). */
32
- isTTY?: boolean;
33
- /** Dry run — show what would change and exit 0 without making changes. */
34
- check?: boolean;
35
- /** Override update channel: 'stable' | 'next'. Falls back to vault.yml update_channel. */
36
- channel?: 'stable' | 'next';
37
- /** Mock fetch for tests. */
38
- fetchFn?: typeof fetch;
39
- /** Injectable vault-sync function for tests. */
40
- vaultSyncFn?: (
41
- vaultDir: string,
42
- opts: Record<string, unknown>,
43
- ) => Promise<{ filesAdded: number; filesRemoved: number }>;
44
- /** Injectable binary install function for tests. */
45
- installBinaryFn?: (version: string) => Promise<void>;
46
- /** Injectable binary validation function for tests. Returns true if binary is valid. */
47
- validateBinaryFn?: () => Promise<boolean>;
48
- /** Injectable register-hooks function for tests. */
49
- registerHooksFn?: (vaultDir: string) => Promise<void>;
50
- }
51
-
52
- export interface UpdateResult {
53
- ok: boolean;
54
- exitCode: number;
55
- latestVersion?: string;
56
- currentVersion?: string;
57
- error?: string;
58
- }
59
-
60
- // ---------------------------------------------------------------------------
61
- // Constants
62
- // ---------------------------------------------------------------------------
63
-
64
- const GITHUB_RELEASES_URL = 'https://api.github.com/repos/kengio/onebrain/releases/latest';
65
-
66
- // ---------------------------------------------------------------------------
67
- // Helpers
68
- // ---------------------------------------------------------------------------
69
-
70
- /** Resolve branch name from channel or vault.yml update_channel. */
71
- function resolveBranch(channel: string | undefined): string {
72
- return channel === 'next' ? 'next' : 'main';
73
- }
74
-
75
- /** Read vault.yml as raw object — non-throwing. Returns {} on missing/invalid. */
76
- async function readVaultYmlRaw(vaultDir: string): Promise<Record<string, unknown>> {
77
- try {
78
- const text = await readFile(join(vaultDir, 'vault.yml'), 'utf8');
79
- return (parseYaml(text) ?? {}) as Record<string, unknown>;
80
- } catch {
81
- return {};
82
- }
83
- }
84
-
85
- /** Write onebrain_version field to vault.yml atomically (in-place merge). */
86
- async function writeVersionToVaultYml(vaultDir: string, version: string): Promise<void> {
87
- const raw = await readVaultYmlRaw(vaultDir);
88
- raw.onebrain_version = version;
89
- const content = stringifyYaml(raw, { lineWidth: 0 });
90
- const vaultYmlPath = join(vaultDir, 'vault.yml');
91
- const tmpPath = `${vaultYmlPath}.tmp`;
92
- await writeFile(tmpPath, content, 'utf8');
93
- await rename(tmpPath, vaultYmlPath);
94
- }
95
-
96
- // ---------------------------------------------------------------------------
97
- // Step 1: Fetch latest release
98
- // ---------------------------------------------------------------------------
99
-
100
- async function fetchLatestVersion(fetchFn: typeof fetch): Promise<string> {
101
- const response = await fetchFn(GITHUB_RELEASES_URL, {
102
- headers: { Accept: 'application/vnd.github.v3+json' },
103
- });
104
- if (!response.ok) {
105
- throw new Error(`GitHub API returned HTTP ${response.status}`);
106
- }
107
- const json = (await response.json()) as Record<string, unknown>;
108
- const tagName = json.tag_name;
109
- if (typeof tagName !== 'string' || !tagName) {
110
- throw new Error('GitHub response missing tag_name');
111
- }
112
- return tagName;
113
- }
114
-
115
- // ---------------------------------------------------------------------------
116
- // Step 4: Install binary
117
- // ---------------------------------------------------------------------------
118
-
119
- async function defaultInstallBinary(version: string): Promise<void> {
120
- const isWindows = process.platform === 'win32';
121
- const cmd = isWindows
122
- ? ['npm', 'install', '-g', `@onebrain-ai/cli@${version}`]
123
- : ['bun', 'install', '-g', `@onebrain-ai/cli@${version}`];
124
-
125
- const proc = Bun.spawn(cmd, { stdout: 'pipe', stderr: 'pipe' });
126
- const exitCode = await proc.exited;
127
- if (exitCode !== 0) {
128
- const errText = await new Response(proc.stderr).text();
129
- throw new Error(`Binary install failed (exit ${exitCode}): ${errText.trim()}`);
130
- }
131
- }
132
-
133
- // ---------------------------------------------------------------------------
134
- // Step 4b: Validate binary
135
- // ---------------------------------------------------------------------------
136
-
137
- async function defaultValidateBinary(): Promise<boolean> {
138
- try {
139
- const proc = Bun.spawn(['onebrain', '--version'], { stdout: 'pipe', stderr: 'pipe' });
140
- const exitCode = await proc.exited;
141
- if (exitCode !== 0) return false;
142
- const stdout = await new Response(proc.stdout).text();
143
- // Expect version-like output (digits)
144
- return /^\d+\.\d+/.test(stdout.trim());
145
- } catch {
146
- return false;
147
- }
148
- }
149
-
150
- // ---------------------------------------------------------------------------
151
- // Main runUpdate
152
- // ---------------------------------------------------------------------------
153
-
154
- export async function runUpdate(opts: UpdateOptions = {}): Promise<UpdateResult> {
155
- const vaultDir = opts.vaultDir ?? process.cwd();
156
- const isTTY = opts.isTTY ?? process.stdout.isTTY ?? false;
157
- const check = opts.check ?? false;
158
-
159
- const fetchFn = opts.fetchFn ?? globalThis.fetch;
160
-
161
- const vaultSyncFn =
162
- opts.vaultSyncFn ??
163
- (async (dir: string, syncOpts: Record<string, unknown>) => {
164
- const { runVaultSync } = await import('../internal/vault-sync.js');
165
- const result = await runVaultSync(dir, syncOpts);
166
- return { filesAdded: result.filesAdded, filesRemoved: result.filesRemoved };
167
- });
168
-
169
- const installBinaryFn = opts.installBinaryFn ?? defaultInstallBinary;
170
- const validateBinaryFn = opts.validateBinaryFn ?? defaultValidateBinary;
171
-
172
- const registerHooksFn =
173
- opts.registerHooksFn ??
174
- (async (dir: string) => {
175
- const { runRegisterHooks } = await import('../internal/register-hooks.js');
176
- await runRegisterHooks({ vaultDir: dir });
177
- });
178
-
179
- const result: UpdateResult = {
180
- ok: false,
181
- exitCode: 0,
182
- };
183
-
184
- // Output helpers
185
- function writeLine(msg: string) {
186
- process.stdout.write(`${msg}\n`);
187
- }
188
-
189
- function noteStep(label: string, detail: string) {
190
- if (isTTY) {
191
- log.step(`${label}\n│ ${detail}`);
192
- } else {
193
- writeLine(`${label}: ${detail}`);
194
- }
195
- }
196
-
197
- // Header
198
- if (isTTY) {
199
- intro('OneBrain Update');
200
- } else {
201
- writeLine('OneBrain Update');
202
- }
203
-
204
- // ── Step 1: Fetch latest release ──────────────────────────────────────────
205
-
206
- let latestVersion: string;
207
- try {
208
- latestVersion = await fetchLatestVersion(fetchFn);
209
- } catch (err) {
210
- const msg = err instanceof Error ? err.message : String(err);
211
- result.error = `Fetch failed: ${msg}`;
212
- result.exitCode = 1;
213
- process.stderr.write(`update: ${result.error}\n`);
214
- return result;
215
- }
216
-
217
- result.latestVersion = latestVersion;
218
-
219
- // Read current version from vault.yml
220
- const vaultYmlRaw = await readVaultYmlRaw(vaultDir);
221
- const currentVersion =
222
- typeof vaultYmlRaw.onebrain_version === 'string' ? vaultYmlRaw.onebrain_version : 'unknown';
223
- result.currentVersion = currentVersion;
224
-
225
- // Resolve channel/branch
226
- const channel =
227
- opts.channel ??
228
- (typeof vaultYmlRaw.update_channel === 'string'
229
- ? (vaultYmlRaw.update_channel as 'stable' | 'next')
230
- : 'stable');
231
- const branch = resolveBranch(channel);
232
-
233
- noteStep('fetching', `${latestVersion} available (current: ${currentVersion})`);
234
-
235
- // ── --check: dry run ──────────────────────────────────────────────────────
236
-
237
- if (check) {
238
- if (isTTY) {
239
- outro('Dry run complete — no changes made');
240
- } else {
241
- writeLine('done: dry run complete — no changes made');
242
- }
243
- result.ok = true;
244
- result.exitCode = 0;
245
- return result;
246
- }
247
-
248
- // ── Step 2: vault-sync ────────────────────────────────────────────────────
249
-
250
- let filesAdded = 0;
251
- let filesRemoved = 0;
252
- try {
253
- const syncResult = await vaultSyncFn(vaultDir, { branch });
254
- filesAdded = syncResult.filesAdded;
255
- filesRemoved = syncResult.filesRemoved;
256
- } catch (err) {
257
- const msg = err instanceof Error ? err.message : String(err);
258
- result.error = `vault-sync failed: ${msg}`;
259
- result.exitCode = 1;
260
- process.stderr.write(`update: ${result.error}\n`);
261
- return result;
262
- }
263
-
264
- noteStep('syncing', `${filesAdded} files synced, ${filesRemoved} removed`);
265
-
266
- // ── Step 4: Install binary ────────────────────────────────────────────────
267
-
268
- try {
269
- await installBinaryFn(latestVersion);
270
- } catch (err) {
271
- const msg = err instanceof Error ? err.message : String(err);
272
- result.error = `Binary install failed: ${msg}`;
273
- result.exitCode = 1;
274
- process.stderr.write(`update: ${result.error}\n`);
275
- return result;
276
- }
277
-
278
- noteStep('upgrading', `@onebrain-ai/cli ${latestVersion} installed`);
279
-
280
- // ── Step 4b: Validate binary (ATOMIC GATE) ────────────────────────────────
281
-
282
- const binaryValid = await validateBinaryFn();
283
- if (!binaryValid) {
284
- result.error = 'Binary validation failed. Check PATH. register-hooks NOT called.';
285
- result.exitCode = 1;
286
- process.stderr.write(`update: ${result.error}\n`);
287
- return result;
288
- }
289
-
290
- // ── Step 5: Register hooks (only if 4b passed) ────────────────────────────
291
-
292
- let hooksDetail = 'hooks: ✓ PATH: ✓ permissions: ✓';
293
- let hooksOk = true;
294
- try {
295
- await registerHooksFn(vaultDir);
296
- } catch (err) {
297
- const msg = err instanceof Error ? err.message : String(err);
298
- hooksDetail = `warning: ${msg}`;
299
- hooksOk = false;
300
- process.stderr.write(`update: register-hooks warning: ${msg}\n`);
301
- }
302
-
303
- if (isTTY) {
304
- log.step(`Registering hooks\n│ ${hooksDetail}`);
305
- } else {
306
- writeLine(hooksOk ? 'hooks: ok PATH: ok permissions: ok' : `hooks: warning — ${hooksDetail}`);
307
- }
308
-
309
- // ── Step 6: Write version to vault.yml ───────────────────────────────────
310
-
311
- try {
312
- await writeVersionToVaultYml(vaultDir, latestVersion);
313
- } catch (err) {
314
- const msg = err instanceof Error ? err.message : String(err);
315
- process.stderr.write(`update: vault.yml version write warning: ${msg}\n`);
316
- if (isTTY) {
317
- log.warn(`vault.yml not updated — ${msg}`);
318
- } else {
319
- writeLine(`warning: vault.yml not updated — ${msg}`);
320
- }
321
- }
322
-
323
- // ── Done ──────────────────────────────────────────────────────────────────
324
-
325
- result.ok = true;
326
- result.exitCode = 0;
327
-
328
- const doneMsg = `OneBrain ${latestVersion}`;
329
- if (isTTY) {
330
- outro(`Done — ${doneMsg}`);
331
- } else {
332
- writeLine(`done: ${doneMsg}`);
333
- }
334
-
335
- return result;
336
- }
337
-
338
- // ---------------------------------------------------------------------------
339
- // CLI entry point (called from index.ts)
340
- // ---------------------------------------------------------------------------
341
-
342
- export interface UpdateCommandOptions {
343
- vaultDir?: string;
344
- check?: boolean;
345
- channel?: 'stable' | 'next';
346
- }
347
-
348
- export async function updateCommand(opts: UpdateCommandOptions = {}): Promise<void> {
349
- const result = await runUpdate(opts);
350
- if (!result.ok) {
351
- process.exit(result.exitCode || 1);
352
- }
353
- }
package/src/index.ts DELETED
@@ -1,144 +0,0 @@
1
- #!/usr/bin/env bun
2
- import { Command } from 'commander';
3
- import { doctorCommand } from './commands/doctor.js';
4
- import { initCommand } from './commands/init.js';
5
- import { updateCommand } from './commands/update.js';
6
- import { checkpointCommand } from './internal/checkpoint.js';
7
- import { migrateCommand } from './internal/migrate.js';
8
- import { orphanScanCommand } from './internal/orphan-scan.js';
9
- import { qmdReindexCommand } from './internal/qmd-reindex.js';
10
- import { registerHooksCommand } from './internal/register-hooks.js';
11
- import { resolveSessionToken, sessionInitCommand } from './internal/session-init.js';
12
- import { vaultSyncCommand } from './internal/vault-sync.js';
13
-
14
- // BUILD_VERSION and BUILD_DATE are injected as string literals at compile time
15
- // via `bun build --define BUILD_VERSION='"x.y.z"'`. When running without --define
16
- // (e.g. `bun run src/index.ts` during development), the identifiers are undeclared
17
- // at runtime, and the typeof guard falls back to the dev placeholder.
18
- declare const BUILD_VERSION: string;
19
- declare const BUILD_DATE: string;
20
- const VERSION = typeof BUILD_VERSION !== 'undefined' ? BUILD_VERSION : '0.0.0-dev';
21
- const RELEASE_DATE = typeof BUILD_DATE !== 'undefined' ? BUILD_DATE : 'dev';
22
-
23
- const VERSION_STRING = `OneBrain v${VERSION} — released ${RELEASE_DATE}`;
24
-
25
- // Handle no-args case before commander parses anything.
26
- if (process.argv.slice(2).length === 0) {
27
- console.log(VERSION_STRING);
28
- console.log('Run `onebrain help` for available commands.');
29
- process.exit(0);
30
- }
31
-
32
- const program = new Command();
33
-
34
- program
35
- .name('onebrain')
36
- .description('OneBrain CLI — personal AI OS for Obsidian')
37
- .version(VERSION_STRING, '-v, --version');
38
-
39
- // ── User-facing commands ──────────────────────────────────────────────────────
40
-
41
- program
42
- .command('init')
43
- .description('Initialize a new OneBrain vault')
44
- .option('--vault-dir <path>', 'vault root directory (default: cwd)')
45
- .option('--harness <harness>', 'harness type: claude-code | gemini | direct')
46
- .option('--force', 'overwrite existing vault.yml without prompting')
47
- .action(async (opts: { vaultDir?: string; harness?: string; force?: boolean }) => {
48
- await initCommand({
49
- vaultDir: opts.vaultDir,
50
- harness: opts.harness as 'claude-code' | 'gemini' | 'direct' | undefined,
51
- force: opts.force,
52
- });
53
- });
54
-
55
- program
56
- .command('update')
57
- .description('Update OneBrain plugin files from GitHub')
58
- .option('--check', 'show what would change and exit without making changes')
59
- .option('--channel <channel>', 'update channel: stable | next')
60
- .action(async (opts: { check?: boolean; channel?: string }) => {
61
- await updateCommand({
62
- check: opts.check,
63
- channel: opts.channel as 'stable' | 'next' | undefined,
64
- });
65
- });
66
-
67
- program
68
- .command('doctor')
69
- .description('Run vault health checks and report issues')
70
- .action(async () => {
71
- const vaultRoot = process.cwd();
72
- await doctorCommand({ vaultDir: vaultRoot, binaryVersion: VERSION });
73
- });
74
-
75
- program
76
- .command('help')
77
- .description('Show this help message')
78
- .action(() => {
79
- program.help();
80
- });
81
-
82
- // ── Internal hidden commands (not shown in --help) ────────────────────────────
83
-
84
- program
85
- .command('session-init', { hidden: true })
86
- .description('Emit session token and datetime (called by Claude Code hook)')
87
- .action(async () => {
88
- const vaultRoot = process.cwd();
89
- await sessionInitCommand(vaultRoot);
90
- });
91
-
92
- program
93
- .command('orphan-scan', { hidden: true })
94
- .description('Scan for orphaned checkpoint files in logs folder')
95
- .argument('<logs_folder>', 'path to logs folder')
96
- .argument('<session_token>', 'current session token to exclude')
97
- .action(async (logsFolder: string, sessionToken: string) => {
98
- await orphanScanCommand(logsFolder, sessionToken);
99
- });
100
-
101
- program
102
- .command('checkpoint', { hidden: true })
103
- .description('Handle checkpoint lifecycle (stop/precompact/postcompact/reset)')
104
- .argument('<mode>', 'stop | precompact | postcompact | reset')
105
- .action(async (mode: string) => {
106
- const token = await resolveSessionToken();
107
- await checkpointCommand(mode, token, process.cwd());
108
- });
109
-
110
- program
111
- .command('qmd-reindex', { hidden: true })
112
- .description('Trigger qmd index rebuild')
113
- .action(async () => {
114
- const vaultRoot = process.cwd();
115
- await qmdReindexCommand(vaultRoot);
116
- });
117
-
118
- program
119
- .command('vault-sync', { hidden: true })
120
- .description('Sync plugin files from GitHub to vault')
121
- .argument('[vault_root]', 'vault root directory (default: cwd)')
122
- .option('--branch <branch>', 'override branch (main | next)')
123
- .action(async (vaultRoot: string | undefined, opts: { branch?: string }) => {
124
- const root = vaultRoot ?? process.cwd();
125
- await vaultSyncCommand(root, { branch: opts.branch });
126
- });
127
-
128
- program
129
- .command('register-hooks', { hidden: true })
130
- .description('Install Claude Code hooks into settings.json')
131
- .option('--vault-dir <path>', 'vault root directory (default: cwd)')
132
- .action(async (opts: { vaultDir?: string }) => {
133
- await registerHooksCommand(opts.vaultDir);
134
- });
135
-
136
- program
137
- .command('migrate', { hidden: true })
138
- .description('Run one-time migration scripts')
139
- .argument('<name>', 'migration name: backfill-recapped')
140
- .action(async (name: string) => {
141
- await migrateCommand(name);
142
- });
143
-
144
- program.parse(process.argv);
@@ -1,12 +0,0 @@
1
- // Bun Snapshot v1, https://bun.sh/docs/test/snapshots
2
-
3
- exports[`handleStop stop block JSON shape matches snapshot { decision: "block", reason: "...-checkpoint-NN.md since ..." } 1`] = `
4
- [
5
- "decision",
6
- "reason",
7
- ]
8
- `;
9
-
10
- exports[`handleStop stop block JSON shape matches snapshot { decision: "block", reason: "...-checkpoint-NN.md since ..." } 2`] = `"block"`;
11
-
12
- exports[`handleStop stop block JSON shape matches snapshot { decision: "block", reason: "...-checkpoint-NN.md since ..." } 3`] = `"string"`;
@@ -1,13 +0,0 @@
1
- // Bun Snapshot v1, https://bun.sh/docs/test/snapshots
2
-
3
- exports[`runOrphanScan output shape matches snapshot { orphan_count: N } 1`] = `
4
- {
5
- "orphan_count": 0,
6
- }
7
- `;
8
-
9
- exports[`runOrphanScan output shape matches snapshot { orphan_count: N } 2`] = `
10
- {
11
- "orphan_count": 1,
12
- }
13
- `;
@@ -1,15 +0,0 @@
1
- // Bun Snapshot v1, https://bun.sh/docs/test/snapshots
2
-
3
- exports[`runSessionInit normal payload output shape matches snapshot 1`] = `
4
- [
5
- "datetime",
6
- "qmd_unembedded",
7
- "session_token",
8
- ]
9
- `;
10
-
11
- exports[`runSessionInit normal payload output shape matches snapshot 2`] = `"string"`;
12
-
13
- exports[`runSessionInit normal payload output shape matches snapshot 3`] = `"string"`;
14
-
15
- exports[`runSessionInit normal payload output shape matches snapshot 4`] = `"number"`;