@brainjar/cli 0.1.0 → 0.2.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/README.md +46 -2
- package/package.json +4 -1
- package/src/cli.ts +6 -0
- package/src/commands/hooks.ts +41 -0
- package/src/commands/pack.ts +49 -0
- package/src/commands/sync.ts +16 -0
- package/src/hooks.test.ts +132 -0
- package/src/hooks.ts +137 -0
- package/src/pack.test.ts +472 -0
- package/src/pack.ts +671 -0
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# brainjar
|
|
2
2
|
|
|
3
3
|
[](https://github.com/brainjar-sh/brainjar-cli/actions/workflows/ci.yml)
|
|
4
|
-
[](https://www.npmjs.com/package/@brainjar/cli)
|
|
5
5
|
[](LICENSE)
|
|
6
6
|
|
|
7
7
|
Shape how your AI thinks — identity, soul, persona, rules.
|
|
@@ -12,7 +12,7 @@ brainjar manages AI agent behavior through composable layers. Instead of one mon
|
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
# Install
|
|
15
|
-
bun install -g brainjar
|
|
15
|
+
bun install -g @brainjar/cli
|
|
16
16
|
|
|
17
17
|
# Initialize with starter content
|
|
18
18
|
brainjar init --default
|
|
@@ -247,11 +247,53 @@ brainjar status
|
|
|
247
247
|
# rules default (global), no-delete (+local)
|
|
248
248
|
```
|
|
249
249
|
|
|
250
|
+
## Pack
|
|
251
|
+
|
|
252
|
+
Packs are self-contained, shareable bundles of a brain and all its layers — soul, persona, and rules. Export a brain as a pack directory, hand it to a teammate, and they import it in one command.
|
|
253
|
+
|
|
254
|
+
A pack mirrors the `~/.brainjar/` structure with a `pack.yaml` manifest at the root. No tarballs, no magic — just files you can inspect with `ls` and `cat`.
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
# Export a brain as a pack
|
|
258
|
+
brainjar pack export review # creates ./review/
|
|
259
|
+
brainjar pack export review --out /tmp # creates /tmp/review/
|
|
260
|
+
brainjar pack export review --name my-review # override pack name
|
|
261
|
+
brainjar pack export review --version 1.0.0 # set version (default: 0.1.0)
|
|
262
|
+
brainjar pack export review --author frank # set author field
|
|
263
|
+
|
|
264
|
+
# Import a pack
|
|
265
|
+
brainjar pack import ./review # import into ~/.brainjar/
|
|
266
|
+
brainjar pack import ./review --force # overwrite conflicts
|
|
267
|
+
brainjar pack import ./review --merge # rename conflicts as <name>-from-<packname>
|
|
268
|
+
brainjar pack import ./review --activate # activate the brain after import
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
On conflict (a file already exists with different content), import fails by default and lists the conflicts. Use `--force` to overwrite or `--merge` to keep both versions. Identical files are silently skipped.
|
|
272
|
+
|
|
273
|
+
## Hooks
|
|
274
|
+
|
|
275
|
+
brainjar integrates with Claude Code's hook system for automatic context injection. When hooks are installed, brainjar syncs your config on every session start — no manual `brainjar sync` needed.
|
|
276
|
+
|
|
277
|
+
```bash
|
|
278
|
+
# Install hooks (writes to ~/.claude/settings.json)
|
|
279
|
+
brainjar hooks install
|
|
280
|
+
|
|
281
|
+
# Install for this project only
|
|
282
|
+
brainjar hooks install --local
|
|
283
|
+
|
|
284
|
+
# Check hook status
|
|
285
|
+
brainjar hooks status
|
|
286
|
+
|
|
287
|
+
# Remove hooks
|
|
288
|
+
brainjar hooks remove
|
|
289
|
+
```
|
|
290
|
+
|
|
250
291
|
## Commands
|
|
251
292
|
|
|
252
293
|
```
|
|
253
294
|
brainjar init [--default] [--obsidian] [--backend claude|codex]
|
|
254
295
|
brainjar status [--sync] [--global|--local] [--short]
|
|
296
|
+
brainjar sync [--quiet]
|
|
255
297
|
brainjar compose <brain> [--task <text>]
|
|
256
298
|
brainjar compose --persona <name> [--task <text>]
|
|
257
299
|
|
|
@@ -261,6 +303,8 @@ brainjar persona create|list|show|use|drop
|
|
|
261
303
|
brainjar rules create|list|show|add|remove
|
|
262
304
|
|
|
263
305
|
brainjar identity create|list|show|use|drop|unlock|get|status|lock
|
|
306
|
+
brainjar pack export|import
|
|
307
|
+
brainjar hooks install|remove|status [--local]
|
|
264
308
|
brainjar shell [--brain|--soul|--persona|--identity|--rules-add|--rules-remove]
|
|
265
309
|
brainjar reset [--backend claude|codex]
|
|
266
310
|
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brainjar/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Shape how your AI thinks — composable identity, soul, persona, and rules for AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -43,6 +43,9 @@
|
|
|
43
43
|
"changeset:tag": "changeset tag",
|
|
44
44
|
"changeset:publish": "changeset publish"
|
|
45
45
|
},
|
|
46
|
+
"overrides": {
|
|
47
|
+
"hono": ">=4.12.7"
|
|
48
|
+
},
|
|
46
49
|
"dependencies": {
|
|
47
50
|
"incur": "^0.3.4",
|
|
48
51
|
"yaml": "^2.8.2"
|
package/src/cli.ts
CHANGED
|
@@ -11,6 +11,9 @@ import { status } from './commands/status.js'
|
|
|
11
11
|
import { reset } from './commands/reset.js'
|
|
12
12
|
import { shell } from './commands/shell.js'
|
|
13
13
|
import { compose } from './commands/compose.js'
|
|
14
|
+
import { sync } from './commands/sync.js'
|
|
15
|
+
import { hooks } from './commands/hooks.js'
|
|
16
|
+
import { pack } from './commands/pack.js'
|
|
14
17
|
|
|
15
18
|
Cli.create('brainjar', {
|
|
16
19
|
description: 'Shape how your AI thinks — identity, soul, persona, rules',
|
|
@@ -27,4 +30,7 @@ Cli.create('brainjar', {
|
|
|
27
30
|
.command(reset)
|
|
28
31
|
.command(shell)
|
|
29
32
|
.command(compose)
|
|
33
|
+
.command(sync)
|
|
34
|
+
.command(hooks)
|
|
35
|
+
.command(pack)
|
|
30
36
|
.serve()
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Cli, z } from 'incur'
|
|
2
|
+
import { installHooks, removeHooks, getHooksStatus } from '../hooks.js'
|
|
3
|
+
|
|
4
|
+
const localOption = z.object({
|
|
5
|
+
local: z.boolean().default(false).describe('Target project-local .claude/settings.json'),
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
const install = Cli.create('install', {
|
|
9
|
+
description: 'Register brainjar hooks in Claude Code settings',
|
|
10
|
+
options: localOption,
|
|
11
|
+
async run(c) {
|
|
12
|
+
return installHooks({ local: c.options.local })
|
|
13
|
+
},
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const remove = Cli.create('remove', {
|
|
17
|
+
description: 'Remove brainjar hooks from Claude Code settings',
|
|
18
|
+
options: localOption,
|
|
19
|
+
async run(c) {
|
|
20
|
+
return removeHooks({ local: c.options.local })
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
const status = Cli.create('status', {
|
|
25
|
+
description: 'Show brainjar hook installation status',
|
|
26
|
+
options: localOption,
|
|
27
|
+
async run(c) {
|
|
28
|
+
const result = await getHooksStatus({ local: c.options.local })
|
|
29
|
+
if (Object.keys(result.hooks).length === 0) {
|
|
30
|
+
return { ...result, installed: false }
|
|
31
|
+
}
|
|
32
|
+
return { ...result, installed: true }
|
|
33
|
+
},
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
export const hooks = Cli.create('hooks', {
|
|
37
|
+
description: 'Manage Claude Code hooks for brainjar',
|
|
38
|
+
})
|
|
39
|
+
.command(install)
|
|
40
|
+
.command(remove)
|
|
41
|
+
.command(status)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Cli, z } from 'incur'
|
|
2
|
+
import { resolve } from 'node:path'
|
|
3
|
+
import { exportPack, importPack } from '../pack.js'
|
|
4
|
+
|
|
5
|
+
const exportCmd = Cli.create('export', {
|
|
6
|
+
description: 'Export a brain as a shareable pack directory',
|
|
7
|
+
args: z.object({
|
|
8
|
+
brain: z.string().describe('Brain name to export'),
|
|
9
|
+
}),
|
|
10
|
+
options: z.object({
|
|
11
|
+
out: z.string().optional().describe('Parent directory for the exported pack (default: cwd)'),
|
|
12
|
+
name: z.string().optional().describe('Override pack name (and output directory name)'),
|
|
13
|
+
version: z.string().optional().describe('Semver version string (default: 0.1.0)'),
|
|
14
|
+
author: z.string().optional().describe('Author field in manifest'),
|
|
15
|
+
}),
|
|
16
|
+
async run(c) {
|
|
17
|
+
return exportPack(c.args.brain, {
|
|
18
|
+
out: c.options.out ? resolve(c.options.out) : undefined,
|
|
19
|
+
name: c.options.name,
|
|
20
|
+
version: c.options.version,
|
|
21
|
+
author: c.options.author,
|
|
22
|
+
})
|
|
23
|
+
},
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const importCmd = Cli.create('import', {
|
|
27
|
+
description: 'Import a pack directory into ~/.brainjar/',
|
|
28
|
+
args: z.object({
|
|
29
|
+
path: z.string().describe('Path to pack directory'),
|
|
30
|
+
}),
|
|
31
|
+
options: z.object({
|
|
32
|
+
force: z.boolean().default(false).describe('Overwrite existing files on conflict'),
|
|
33
|
+
merge: z.boolean().default(false).describe('Rename incoming files on conflict as <name>-from-<packname>'),
|
|
34
|
+
activate: z.boolean().default(false).describe('Activate the brain after successful import'),
|
|
35
|
+
}),
|
|
36
|
+
async run(c) {
|
|
37
|
+
return importPack(resolve(c.args.path), {
|
|
38
|
+
force: c.options.force,
|
|
39
|
+
merge: c.options.merge,
|
|
40
|
+
activate: c.options.activate,
|
|
41
|
+
})
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
export const pack = Cli.create('pack', {
|
|
46
|
+
description: 'Export and import brainjar packs — self-contained shareable bundles',
|
|
47
|
+
})
|
|
48
|
+
.command(exportCmd)
|
|
49
|
+
.command(importCmd)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Cli, z } from 'incur'
|
|
2
|
+
import { sync as runSync } from '../sync.js'
|
|
3
|
+
import { requireBrainjarDir } from '../state.js'
|
|
4
|
+
|
|
5
|
+
export const sync = Cli.create('sync', {
|
|
6
|
+
description: 'Regenerate config file from active layers',
|
|
7
|
+
options: z.object({
|
|
8
|
+
quiet: z.boolean().default(false).describe('Suppress output (for use in hooks)'),
|
|
9
|
+
}),
|
|
10
|
+
async run(c) {
|
|
11
|
+
await requireBrainjarDir()
|
|
12
|
+
const result = await runSync()
|
|
13
|
+
if (c.options.quiet) return
|
|
14
|
+
return result
|
|
15
|
+
},
|
|
16
|
+
})
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { describe, test, expect, beforeEach, afterEach } from 'bun:test'
|
|
2
|
+
import { readFile, rm, mkdir, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import { installHooks, removeHooks, getHooksStatus } from './hooks.js'
|
|
5
|
+
|
|
6
|
+
const TEST_HOME = join(import.meta.dir, '..', '.test-home-hooks')
|
|
7
|
+
const SETTINGS_PATH = join(TEST_HOME, '.claude', 'settings.json')
|
|
8
|
+
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
process.env.BRAINJAR_TEST_HOME = TEST_HOME
|
|
11
|
+
await mkdir(join(TEST_HOME, '.claude'), { recursive: true })
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
delete process.env.BRAINJAR_TEST_HOME
|
|
16
|
+
await rm(TEST_HOME, { recursive: true, force: true })
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
describe('hooks install', () => {
|
|
20
|
+
test('creates hooks in empty settings', async () => {
|
|
21
|
+
const result = await installHooks()
|
|
22
|
+
expect(result.installed).toContain('SessionStart')
|
|
23
|
+
|
|
24
|
+
const settings = JSON.parse(await readFile(SETTINGS_PATH, 'utf-8'))
|
|
25
|
+
expect(settings.hooks.SessionStart).toHaveLength(1)
|
|
26
|
+
expect(settings.hooks.SessionStart[0].matcher).toBe('startup')
|
|
27
|
+
expect(settings.hooks.SessionStart[0].hooks[0].command).toBe('brainjar sync --quiet')
|
|
28
|
+
expect(settings.hooks.SessionStart[0].hooks[0]._brainjar).toBe(true)
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
test('preserves existing settings', async () => {
|
|
32
|
+
await writeFile(SETTINGS_PATH, JSON.stringify({
|
|
33
|
+
statusLine: { type: 'command', command: 'echo hi' },
|
|
34
|
+
enabledPlugins: { foo: true },
|
|
35
|
+
}))
|
|
36
|
+
|
|
37
|
+
await installHooks()
|
|
38
|
+
|
|
39
|
+
const settings = JSON.parse(await readFile(SETTINGS_PATH, 'utf-8'))
|
|
40
|
+
expect(settings.statusLine.command).toBe('echo hi')
|
|
41
|
+
expect(settings.enabledPlugins.foo).toBe(true)
|
|
42
|
+
expect(settings.hooks.SessionStart).toHaveLength(1)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('preserves existing non-brainjar hooks', async () => {
|
|
46
|
+
await writeFile(SETTINGS_PATH, JSON.stringify({
|
|
47
|
+
hooks: {
|
|
48
|
+
SessionStart: [
|
|
49
|
+
{ matcher: 'startup', hooks: [{ type: 'command', command: 'echo hello' }] },
|
|
50
|
+
],
|
|
51
|
+
PreToolUse: [
|
|
52
|
+
{ matcher: 'Edit', hooks: [{ type: 'command', command: 'lint.sh' }] },
|
|
53
|
+
],
|
|
54
|
+
},
|
|
55
|
+
}))
|
|
56
|
+
|
|
57
|
+
await installHooks()
|
|
58
|
+
|
|
59
|
+
const settings = JSON.parse(await readFile(SETTINGS_PATH, 'utf-8'))
|
|
60
|
+
// Existing non-brainjar SessionStart hook preserved
|
|
61
|
+
expect(settings.hooks.SessionStart).toHaveLength(2)
|
|
62
|
+
// PreToolUse untouched
|
|
63
|
+
expect(settings.hooks.PreToolUse).toHaveLength(1)
|
|
64
|
+
expect(settings.hooks.PreToolUse[0].hooks[0].command).toBe('lint.sh')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('is idempotent — no duplicates on re-install', async () => {
|
|
68
|
+
await installHooks()
|
|
69
|
+
await installHooks()
|
|
70
|
+
|
|
71
|
+
const settings = JSON.parse(await readFile(SETTINGS_PATH, 'utf-8'))
|
|
72
|
+
const brainjarEntries = settings.hooks.SessionStart.filter(
|
|
73
|
+
(m: any) => m.hooks.some((h: any) => h._brainjar)
|
|
74
|
+
)
|
|
75
|
+
expect(brainjarEntries).toHaveLength(1)
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
describe('hooks remove', () => {
|
|
80
|
+
test('removes brainjar hooks', async () => {
|
|
81
|
+
await installHooks()
|
|
82
|
+
const result = await removeHooks()
|
|
83
|
+
|
|
84
|
+
expect(result.removed).toContain('SessionStart')
|
|
85
|
+
|
|
86
|
+
const settings = JSON.parse(await readFile(SETTINGS_PATH, 'utf-8'))
|
|
87
|
+
expect(settings.hooks).toBeUndefined()
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
test('preserves non-brainjar hooks', async () => {
|
|
91
|
+
await writeFile(SETTINGS_PATH, JSON.stringify({
|
|
92
|
+
hooks: {
|
|
93
|
+
SessionStart: [
|
|
94
|
+
{ matcher: 'startup', hooks: [{ type: 'command', command: 'echo hello' }] },
|
|
95
|
+
],
|
|
96
|
+
},
|
|
97
|
+
}))
|
|
98
|
+
|
|
99
|
+
await installHooks()
|
|
100
|
+
await removeHooks()
|
|
101
|
+
|
|
102
|
+
const settings = JSON.parse(await readFile(SETTINGS_PATH, 'utf-8'))
|
|
103
|
+
expect(settings.hooks.SessionStart).toHaveLength(1)
|
|
104
|
+
expect(settings.hooks.SessionStart[0].hooks[0].command).toBe('echo hello')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('no-op when no brainjar hooks exist', async () => {
|
|
108
|
+
await writeFile(SETTINGS_PATH, JSON.stringify({ statusLine: { command: 'echo' } }))
|
|
109
|
+
|
|
110
|
+
const result = await removeHooks()
|
|
111
|
+
expect(result.removed).toHaveLength(0)
|
|
112
|
+
})
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
describe('hooks status', () => {
|
|
116
|
+
test('reports not installed when no hooks', async () => {
|
|
117
|
+
await writeFile(SETTINGS_PATH, JSON.stringify({}))
|
|
118
|
+
const result = await getHooksStatus()
|
|
119
|
+
expect(Object.keys(result.hooks)).toHaveLength(0)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
test('reports installed hooks', async () => {
|
|
123
|
+
await installHooks()
|
|
124
|
+
const result = await getHooksStatus()
|
|
125
|
+
expect(result.hooks.SessionStart).toBe('brainjar sync --quiet')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
test('handles missing settings file', async () => {
|
|
129
|
+
const result = await getHooksStatus()
|
|
130
|
+
expect(Object.keys(result.hooks)).toHaveLength(0)
|
|
131
|
+
})
|
|
132
|
+
})
|
package/src/hooks.ts
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises'
|
|
2
|
+
import { join, dirname } from 'node:path'
|
|
3
|
+
import { getHome } from './paths.js'
|
|
4
|
+
|
|
5
|
+
export interface HookEntry {
|
|
6
|
+
type: 'command'
|
|
7
|
+
command: string
|
|
8
|
+
timeout?: number
|
|
9
|
+
_brainjar?: true
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface HookMatcher {
|
|
13
|
+
matcher?: string
|
|
14
|
+
hooks: HookEntry[]
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface HooksConfig {
|
|
18
|
+
[event: string]: HookMatcher[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface Settings {
|
|
22
|
+
hooks?: HooksConfig
|
|
23
|
+
[key: string]: unknown
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const BRAINJAR_HOOKS: Record<string, HookMatcher> = {
|
|
27
|
+
SessionStart: {
|
|
28
|
+
matcher: 'startup',
|
|
29
|
+
hooks: [
|
|
30
|
+
{
|
|
31
|
+
type: 'command',
|
|
32
|
+
command: 'brainjar sync --quiet',
|
|
33
|
+
timeout: 5000,
|
|
34
|
+
_brainjar: true,
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getSettingsPath(local: boolean): string {
|
|
41
|
+
if (local) return join(process.cwd(), '.claude', 'settings.json')
|
|
42
|
+
return join(getHome(), '.claude', 'settings.json')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function readSettings(path: string): Promise<Settings> {
|
|
46
|
+
try {
|
|
47
|
+
const raw = await readFile(path, 'utf-8')
|
|
48
|
+
return JSON.parse(raw)
|
|
49
|
+
} catch (e) {
|
|
50
|
+
if ((e as NodeJS.ErrnoException).code === 'ENOENT') return {}
|
|
51
|
+
throw e
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function writeSettings(path: string, settings: Settings): Promise<void> {
|
|
56
|
+
await mkdir(dirname(path), { recursive: true })
|
|
57
|
+
await writeFile(path, JSON.stringify(settings, null, 2) + '\n')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function isBrainjarHook(entry: HookEntry): boolean {
|
|
61
|
+
return entry._brainjar === true
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function isBrainjarMatcher(matcher: HookMatcher): boolean {
|
|
65
|
+
return matcher.hooks.some(isBrainjarHook)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function installHooks(options: { local?: boolean } = {}): Promise<{ path: string; installed: string[] }> {
|
|
69
|
+
const path = getSettingsPath(options.local ?? false)
|
|
70
|
+
const settings = await readSettings(path)
|
|
71
|
+
|
|
72
|
+
if (!settings.hooks) settings.hooks = {}
|
|
73
|
+
|
|
74
|
+
const installed: string[] = []
|
|
75
|
+
|
|
76
|
+
for (const [event, matcher] of Object.entries(BRAINJAR_HOOKS)) {
|
|
77
|
+
if (!settings.hooks[event]) settings.hooks[event] = []
|
|
78
|
+
|
|
79
|
+
// Remove any existing brainjar entries for this event
|
|
80
|
+
settings.hooks[event] = settings.hooks[event].filter(m => !isBrainjarMatcher(m))
|
|
81
|
+
|
|
82
|
+
// Add the new one
|
|
83
|
+
settings.hooks[event].push(matcher)
|
|
84
|
+
installed.push(event)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await writeSettings(path, settings)
|
|
88
|
+
return { path, installed }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function removeHooks(options: { local?: boolean } = {}): Promise<{ path: string; removed: string[] }> {
|
|
92
|
+
const path = getSettingsPath(options.local ?? false)
|
|
93
|
+
const settings = await readSettings(path)
|
|
94
|
+
|
|
95
|
+
const removed: string[] = []
|
|
96
|
+
|
|
97
|
+
if (settings.hooks) {
|
|
98
|
+
for (const [event, matchers] of Object.entries(settings.hooks)) {
|
|
99
|
+
const filtered = matchers.filter(m => !isBrainjarMatcher(m))
|
|
100
|
+
if (filtered.length < matchers.length) {
|
|
101
|
+
removed.push(event)
|
|
102
|
+
if (filtered.length === 0) {
|
|
103
|
+
delete settings.hooks[event]
|
|
104
|
+
} else {
|
|
105
|
+
settings.hooks[event] = filtered
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (Object.keys(settings.hooks).length === 0) {
|
|
110
|
+
delete settings.hooks
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
await writeSettings(path, settings)
|
|
115
|
+
return { path, removed }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function getHooksStatus(options: { local?: boolean } = {}): Promise<{ path: string; hooks: Record<string, string> }> {
|
|
119
|
+
const path = getSettingsPath(options.local ?? false)
|
|
120
|
+
const settings = await readSettings(path)
|
|
121
|
+
|
|
122
|
+
const hooks: Record<string, string> = {}
|
|
123
|
+
|
|
124
|
+
if (settings.hooks) {
|
|
125
|
+
for (const [event, matchers] of Object.entries(settings.hooks)) {
|
|
126
|
+
for (const matcher of matchers) {
|
|
127
|
+
for (const hook of matcher.hooks) {
|
|
128
|
+
if (isBrainjarHook(hook)) {
|
|
129
|
+
hooks[event] = hook.command
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { path, hooks }
|
|
137
|
+
}
|