@brainjar/cli 0.1.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/LICENSE +21 -0
- package/README.md +309 -0
- package/package.json +55 -0
- package/src/cli.ts +30 -0
- package/src/commands/brain.ts +256 -0
- package/src/commands/compose.ts +156 -0
- package/src/commands/identity.ts +276 -0
- package/src/commands/init.ts +78 -0
- package/src/commands/persona.ts +259 -0
- package/src/commands/reset.ts +46 -0
- package/src/commands/rules.ts +269 -0
- package/src/commands/shell.ts +119 -0
- package/src/commands/soul.ts +207 -0
- package/src/commands/status.ts +131 -0
- package/src/engines/bitwarden.ts +105 -0
- package/src/engines/index.ts +12 -0
- package/src/engines/types.ts +12 -0
- package/src/paths.ts +48 -0
- package/src/seeds/personas/engineer.md +26 -0
- package/src/seeds/personas/planner.md +24 -0
- package/src/seeds/personas/reviewer.md +27 -0
- package/src/seeds/rules/default/boundaries.md +25 -0
- package/src/seeds/rules/default/context-recovery.md +17 -0
- package/src/seeds/rules/default/task-completion.md +31 -0
- package/src/seeds/rules/git-discipline.md +22 -0
- package/src/seeds/rules/security.md +26 -0
- package/src/seeds/souls/craftsman.md +24 -0
- package/src/seeds/templates/persona.md +19 -0
- package/src/seeds/templates/rule.md +11 -0
- package/src/seeds/templates/soul.md +20 -0
- package/src/seeds.ts +116 -0
- package/src/state.ts +414 -0
- package/src/sync.ts +190 -0
package/src/sync.ts
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { readFile, writeFile, copyFile, mkdir, access } from 'node:fs/promises'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { type Backend, getBackendConfig, paths } from './paths.js'
|
|
4
|
+
import { type State, readState, readLocalState, readEnvState, mergeState, requireBrainjarDir, stripFrontmatter, resolveRuleContent } from './state.js'
|
|
5
|
+
|
|
6
|
+
export const MARKER_START = '<!-- brainjar:start -->'
|
|
7
|
+
export const MARKER_END = '<!-- brainjar:end -->'
|
|
8
|
+
|
|
9
|
+
export interface SyncOptions {
|
|
10
|
+
backend?: Backend
|
|
11
|
+
local?: boolean
|
|
12
|
+
envOverrides?: Record<string, string>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function inlineSoul(name: string, sections: string[]) {
|
|
16
|
+
const raw = await readFile(join(paths.souls, `${name}.md`), 'utf-8')
|
|
17
|
+
const content = stripFrontmatter(raw)
|
|
18
|
+
sections.push('')
|
|
19
|
+
sections.push('## Soul')
|
|
20
|
+
sections.push('')
|
|
21
|
+
sections.push(content)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function inlinePersona(name: string, sections: string[]) {
|
|
25
|
+
const raw = await readFile(join(paths.personas, `${name}.md`), 'utf-8')
|
|
26
|
+
const content = stripFrontmatter(raw)
|
|
27
|
+
sections.push('')
|
|
28
|
+
sections.push('## Persona')
|
|
29
|
+
sections.push('')
|
|
30
|
+
sections.push(content)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async function inlineRules(rules: string[], sections: string[], warnings: string[]) {
|
|
34
|
+
for (const rule of rules) {
|
|
35
|
+
const contents = await resolveRuleContent(rule, warnings)
|
|
36
|
+
for (const content of contents) {
|
|
37
|
+
sections.push('')
|
|
38
|
+
sections.push(content)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function inlineIdentity(name: string, sections: string[]) {
|
|
44
|
+
try {
|
|
45
|
+
await access(join(paths.identities, `${name}.yaml`))
|
|
46
|
+
sections.push('')
|
|
47
|
+
sections.push('## Identity')
|
|
48
|
+
sections.push('')
|
|
49
|
+
sections.push(`See ~/.brainjar/identities/${name}.yaml for active identity.`)
|
|
50
|
+
sections.push('Manage with `brainjar identity [list|use|show]`.')
|
|
51
|
+
} catch {}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Extract content before, inside, and after brainjar markers. */
|
|
55
|
+
function parseMarkers(content: string): { before: string; after: string } | null {
|
|
56
|
+
const startIdx = content.indexOf(MARKER_START)
|
|
57
|
+
const endIdx = content.indexOf(MARKER_END)
|
|
58
|
+
if (startIdx === -1 || endIdx === -1 || endIdx < startIdx) return null
|
|
59
|
+
|
|
60
|
+
const before = content.slice(0, startIdx).trimEnd()
|
|
61
|
+
const after = content.slice(endIdx + MARKER_END.length).trimStart()
|
|
62
|
+
return { before, after }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export async function sync(options?: Backend | SyncOptions) {
|
|
66
|
+
await requireBrainjarDir()
|
|
67
|
+
|
|
68
|
+
// Normalize legacy call signature: sync('claude') → sync({ backend: 'claude' })
|
|
69
|
+
const opts: SyncOptions = typeof options === 'string' ? { backend: options } : options ?? {}
|
|
70
|
+
|
|
71
|
+
const globalState = await readState()
|
|
72
|
+
const backend: Backend = opts.backend ?? (globalState.backend as Backend) ?? 'claude'
|
|
73
|
+
const config = getBackendConfig(backend, { local: opts.local })
|
|
74
|
+
|
|
75
|
+
const envState = readEnvState(opts.envOverrides)
|
|
76
|
+
const warnings: string[] = []
|
|
77
|
+
|
|
78
|
+
// Read existing config file
|
|
79
|
+
let existingContent: string | null = null
|
|
80
|
+
try {
|
|
81
|
+
existingContent = await readFile(config.configFile, 'utf-8')
|
|
82
|
+
} catch (e) {
|
|
83
|
+
const code = (e as NodeJS.ErrnoException).code
|
|
84
|
+
if (code !== 'ENOENT') {
|
|
85
|
+
warnings.push(`Could not read existing config: ${(e as Error).message}`)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Backup existing config if it has no brainjar markers (first-time takeover)
|
|
90
|
+
if (existingContent !== null && !existingContent.includes(MARKER_START)) {
|
|
91
|
+
try {
|
|
92
|
+
await copyFile(config.configFile, config.backupFile)
|
|
93
|
+
} catch (e) {
|
|
94
|
+
warnings.push(`Could not back up existing config: ${(e as Error).message}`)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Build the brainjar section content
|
|
99
|
+
const sections: string[] = []
|
|
100
|
+
|
|
101
|
+
if (opts.local) {
|
|
102
|
+
// Local mode: read local state + env, only write overridden layers.
|
|
103
|
+
// Everything else falls back to the global config (Claude Code merges both files).
|
|
104
|
+
const localState = await readLocalState()
|
|
105
|
+
const effective = mergeState(globalState, localState, envState)
|
|
106
|
+
|
|
107
|
+
if ('soul' in localState && effective.soul.value) {
|
|
108
|
+
await inlineSoul(effective.soul.value, sections)
|
|
109
|
+
}
|
|
110
|
+
if ('persona' in localState && effective.persona.value) {
|
|
111
|
+
await inlinePersona(effective.persona.value, sections)
|
|
112
|
+
}
|
|
113
|
+
if (localState.rules) {
|
|
114
|
+
// Inline the effective rules that are active (not removed)
|
|
115
|
+
const activeRules = effective.rules
|
|
116
|
+
.filter(r => !r.scope.startsWith('-'))
|
|
117
|
+
.map(r => r.value)
|
|
118
|
+
// But only write rules section if local state has rules overrides
|
|
119
|
+
await inlineRules(activeRules, sections, warnings)
|
|
120
|
+
}
|
|
121
|
+
if ('identity' in localState && effective.identity.value) {
|
|
122
|
+
await inlineIdentity(effective.identity.value, sections)
|
|
123
|
+
}
|
|
124
|
+
} else {
|
|
125
|
+
// Global mode: apply env overrides on top of global state, write all layers
|
|
126
|
+
const effective = mergeState(globalState, {}, envState)
|
|
127
|
+
const effectiveSoul = effective.soul.value
|
|
128
|
+
const effectivePersona = effective.persona.value
|
|
129
|
+
const effectiveRules = effective.rules.filter(r => !r.scope.startsWith('-')).map(r => r.value)
|
|
130
|
+
const effectiveIdentity = effective.identity.value
|
|
131
|
+
|
|
132
|
+
if (effectiveSoul) await inlineSoul(effectiveSoul, sections)
|
|
133
|
+
if (effectivePersona) await inlinePersona(effectivePersona, sections)
|
|
134
|
+
await inlineRules(effectiveRules, sections, warnings)
|
|
135
|
+
if (effectiveIdentity) await inlineIdentity(effectiveIdentity, sections)
|
|
136
|
+
|
|
137
|
+
// Local Overrides note (only for global config)
|
|
138
|
+
sections.push('')
|
|
139
|
+
sections.push('## Project-Level Overrides')
|
|
140
|
+
sections.push('')
|
|
141
|
+
sections.push('If a project has its own .claude/CLAUDE.md, those instructions take precedence for project-specific concerns. These global rules still apply for general behavior.')
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Wrap in markers
|
|
145
|
+
const brainjarBlock = [
|
|
146
|
+
MARKER_START,
|
|
147
|
+
`# ${config.configFileName} — Managed by brainjar`,
|
|
148
|
+
...sections,
|
|
149
|
+
'',
|
|
150
|
+
MARKER_END,
|
|
151
|
+
].join('\n')
|
|
152
|
+
|
|
153
|
+
// Splice into existing content or create fresh
|
|
154
|
+
let output: string
|
|
155
|
+
const parsed = existingContent ? parseMarkers(existingContent) : null
|
|
156
|
+
|
|
157
|
+
if (parsed) {
|
|
158
|
+
// Existing file with markers — replace the brainjar section, preserve the rest
|
|
159
|
+
// Discard legacy brainjar content that ended up outside markers during migration
|
|
160
|
+
const before = parsed.before
|
|
161
|
+
const after = parsed.after?.includes('# Managed by brainjar') ? '' : parsed.after
|
|
162
|
+
const parts: string[] = []
|
|
163
|
+
if (before) parts.push(before, '')
|
|
164
|
+
parts.push(brainjarBlock)
|
|
165
|
+
if (after) parts.push('', after)
|
|
166
|
+
output = parts.join('\n')
|
|
167
|
+
} else if (existingContent && !existingContent.includes(MARKER_START)) {
|
|
168
|
+
// Existing file without markers (first sync)
|
|
169
|
+
if (existingContent.includes('# Managed by brainjar')) {
|
|
170
|
+
// Legacy brainjar-managed file — replace entirely
|
|
171
|
+
output = brainjarBlock + '\n'
|
|
172
|
+
} else {
|
|
173
|
+
// User-owned file — prepend brainjar section, preserve user content
|
|
174
|
+
output = brainjarBlock + '\n\n' + existingContent
|
|
175
|
+
}
|
|
176
|
+
} else {
|
|
177
|
+
// No existing file
|
|
178
|
+
output = brainjarBlock + '\n'
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
await mkdir(config.dir, { recursive: true })
|
|
182
|
+
await writeFile(config.configFile, output)
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
backend,
|
|
186
|
+
written: config.configFile,
|
|
187
|
+
local: opts.local ?? false,
|
|
188
|
+
...(warnings.length ? { warnings } : {}),
|
|
189
|
+
}
|
|
190
|
+
}
|