mirror of
https://github.com/AgentSeal/codeburn.git
synced 2026-10-20 03:05:24 +00:00
fix(providers): accept Crush's {"projects":[...]} registry shape (#1510)
Crush's project registry has used a ProjectList wrapper since it was introduced, so loadRegistry parsed a real registry as zero entries and reported no usage. Accept the wrapper alongside the array and keyed-object shapes the parser already handles, and correct the registry-format description in the provider docs.
This commit is contained in:
parent
4d0f02a51c
commit
598e77a808
3 changed files with 65 additions and 13 deletions
|
|
@ -4,7 +4,7 @@ Charmbracelet's Crush TUI coding agent.
|
|||
|
||||
- **Source:** `src/providers/crush.ts`
|
||||
- **Loading:** lazy (`src/providers/index.ts`). Lazy because Crush ships per-project SQLite databases and we use `node:sqlite` to read them.
|
||||
- **Test:** `tests/providers/crush.test.ts` (10 tests, fixture-based)
|
||||
- **Test:** `tests/providers/crush.test.ts` (12 tests, fixture-based)
|
||||
|
||||
## Where it reads from
|
||||
|
||||
|
|
@ -15,7 +15,7 @@ Crush keeps a global registry that lists every project it has touched, and a sep
|
|||
| Registry (project list) | `$CRUSH_GLOBAL_DATA/projects.json`, otherwise `$XDG_DATA_HOME/crush/projects.json`, otherwise `~/.local/share/crush/projects.json` (Linux/macOS) or `%LOCALAPPDATA%/crush/projects.json` (Windows). |
|
||||
| Per-project db | `<project.path>/<project.data_dir>/crush.db` where `data_dir` defaults to `.crush`. |
|
||||
|
||||
The registry shape is an object keyed by project id (modern Crush) or an array (older builds and tokscale's sample fixtures). The parser accepts both.
|
||||
Current Crush writes an object with a `projects` array. The parser also accepts a bare array and an object keyed by project id for compatibility.
|
||||
|
||||
## Storage format
|
||||
|
||||
|
|
|
|||
|
|
@ -54,13 +54,14 @@ async function loadRegistry(path: string): Promise<ProjectEntry[]> {
|
|||
} catch {
|
||||
return []
|
||||
}
|
||||
// Crush writes projects.json as an object keyed by project id. Older builds
|
||||
// (and tokscale's sample fixtures) emit an array. Accept both shapes.
|
||||
let entries: unknown[]
|
||||
if (Array.isArray(parsed)) {
|
||||
entries = parsed
|
||||
} else if (parsed && typeof parsed === 'object') {
|
||||
entries = Object.values(parsed)
|
||||
const registry = parsed as Record<string, unknown>
|
||||
// Crush has used a {"projects": [...]} wrapper since the registry began.
|
||||
// Array and keyed-object forms remain for compatibility with existing users.
|
||||
entries = Array.isArray(registry['projects']) ? registry['projects'] : Object.values(registry)
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
|
|
|
|||
|
|
@ -116,9 +116,17 @@ function insertMessage(db: TestDb, sessionId: string, role: string, model: strin
|
|||
`).run(id, sessionId, role, model, 1_700_000_000, 1_700_000_000)
|
||||
}
|
||||
|
||||
async function writeRegistry(globalDataDir: string, entries: Record<string, { path: string; data_dir: string }>): Promise<void> {
|
||||
type RegistryEntry = {
|
||||
path: string
|
||||
data_dir: string
|
||||
last_accessed?: string
|
||||
}
|
||||
|
||||
type Registry = RegistryEntry[] | Record<string, RegistryEntry> | { projects: RegistryEntry[] }
|
||||
|
||||
async function writeRegistry(globalDataDir: string, registry: Registry): Promise<void> {
|
||||
await mkdir(globalDataDir, { recursive: true })
|
||||
await writeFile(join(globalDataDir, 'projects.json'), JSON.stringify(entries))
|
||||
await writeFile(join(globalDataDir, 'projects.json'), JSON.stringify(registry))
|
||||
}
|
||||
|
||||
async function collect(parser: { parse(): AsyncGenerator<ParsedProviderCall> }): Promise<ParsedProviderCall[]> {
|
||||
|
|
@ -270,7 +278,7 @@ describe('crush provider', () => {
|
|||
expect(second).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('accepts an array-shaped projects.json (legacy format)', async () => {
|
||||
it('accepts an array-shaped projects.json for compatibility', async () => {
|
||||
if (!isSqliteAvailable()) return
|
||||
|
||||
const projectDir = join(tmpRoot, 'project-e')
|
||||
|
|
@ -280,16 +288,59 @@ describe('crush provider', () => {
|
|||
})
|
||||
|
||||
const globalData = join(tmpRoot, 'crush-global')
|
||||
await mkdir(globalData, { recursive: true })
|
||||
await writeFile(
|
||||
join(globalData, 'projects.json'),
|
||||
JSON.stringify([{ path: projectDir, data_dir: '.crush' }]),
|
||||
)
|
||||
await writeRegistry(globalData, [{ path: projectDir, data_dir: '.crush' }])
|
||||
process.env['CRUSH_GLOBAL_DATA'] = globalData
|
||||
|
||||
const p = createCrushProvider()
|
||||
const sources = await p.discoverSessions()
|
||||
expect(sources).toHaveLength(1)
|
||||
expect(sources[0]!.path).toBe(`${dbPath}:sess-arr`)
|
||||
})
|
||||
|
||||
it('accepts an object keyed by project id for compatibility', async () => {
|
||||
if (!isSqliteAvailable()) return
|
||||
|
||||
const projectDir = join(tmpRoot, 'project-f')
|
||||
const dbPath = createCrushDb(join(projectDir, '.crush'))
|
||||
withTestDb(dbPath, db => {
|
||||
insertSession(db, { id: 'sess-keyed', cost: 0.01, promptTokens: 1, completionTokens: 1, createdAt: 1_700_000_900 })
|
||||
})
|
||||
|
||||
const globalData = join(tmpRoot, 'crush-global')
|
||||
await writeRegistry(globalData, {
|
||||
project: { path: projectDir, data_dir: '.crush' },
|
||||
})
|
||||
process.env['CRUSH_GLOBAL_DATA'] = globalData
|
||||
|
||||
const p = createCrushProvider()
|
||||
const sources = await p.discoverSessions()
|
||||
expect(sources).toHaveLength(1)
|
||||
expect(sources[0]!.path).toBe(`${dbPath}:sess-keyed`)
|
||||
})
|
||||
|
||||
it('accepts the current Crush projects wrapper', async () => {
|
||||
if (!isSqliteAvailable()) return
|
||||
|
||||
const projectDir = join(tmpRoot, 'project-g')
|
||||
const dbPath = createCrushDb(join(projectDir, '.crush'))
|
||||
withTestDb(dbPath, db => {
|
||||
insertSession(db, { id: 'sess-wrapped', cost: 0.01, promptTokens: 1, completionTokens: 1, createdAt: 1_700_001_000 })
|
||||
})
|
||||
|
||||
const globalData = join(tmpRoot, 'crush-global')
|
||||
await writeRegistry(globalData, {
|
||||
projects: [{
|
||||
path: projectDir,
|
||||
data_dir: '.crush',
|
||||
last_accessed: '2025-12-15T12:34:56.789012Z',
|
||||
}],
|
||||
})
|
||||
process.env['CRUSH_GLOBAL_DATA'] = globalData
|
||||
|
||||
const p = createCrushProvider()
|
||||
const sources = await p.discoverSessions()
|
||||
expect(sources).toHaveLength(1)
|
||||
expect(sources[0]!.path).toBe(`${dbPath}:sess-wrapped`)
|
||||
})
|
||||
|
||||
it('ignores registry entries whose db is missing', async () => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue