Skip to content

Commit a08f98f

Browse files
fix: consolidate discovery-cache, batch bundle refs, remove dead code (#290)
* fix: consolidate discovery-cache, batch bundle refs, remove dead code * refactor: consolidate discovery cache into generic discoverCached, extract spawnGit helper
1 parent 1f35f2c commit a08f98f

13 files changed

Lines changed: 197 additions & 204 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ For OAuth, Passkeys, Push Notifications (VAPID), and advanced configuration, see
9999

100100
## `ocm` CLI
101101

102-
OpenCode Manager ships an `ocm` CLI (from `ocm-cli/`) that attaches your local OpenCode TUI to a repo hosted on the Manager. It lists ready repos, attaches via the Manager's `/api/opencode-proxy` (so prompts run on the Manager's filesystem against a single shared OpenCode server), and can tarball-sync the working tree up or down with `ocm push` / `ocm pull`. Running `ocm` inside a local clone auto-detects the matching Manager repo by `origin` URL.
102+
OpenCode Manager ships an `ocm` CLI (from `ocm-cli/`) that attaches your local OpenCode TUI to a repo hosted on the Manager. It lists ready repos, attaches via the Manager's `/api/opencode-proxy` (so prompts run on the Manager's filesystem against a single shared OpenCode server), and can sync the working tree up or down with `ocm push` / `ocm pull` (fast git bundle + working-tree patch by default; pass `--full` for the legacy tarball mirror). Running `ocm` inside a local clone auto-detects the matching Manager repo by `origin` URL.
103103

104104
See the [`ocm` CLI guide](docs/ocm-cli.md) for setup and commands.
105105

backend/src/routes/internal/repo-mirror.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
readUploadMeta,
2020
deleteUploadSession,
2121
getPartPath,
22+
getStagingRoot,
2223
extractPartsToStaging,
2324
atomicSwapIntoPlace,
2425
carryOverIgnoredFiles,
@@ -96,6 +97,7 @@ async function applyMirrorPatch(fullPath: string, patch: string): Promise<void>
9697
async function importBundle(fullPath: string, bundlePath: string, branch: string | null): Promise<void> {
9798
await gitRaw(fullPath, ['fetch', bundlePath, '+refs/heads/*:refs/remotes/ocm-sync/*', '+refs/tags/*:refs/tags/*'])
9899
const refs = await gitRaw(fullPath, ['for-each-ref', '--format=%(refname:strip=3) %(objectname)', 'refs/remotes/ocm-sync'])
100+
const updates: string[] = []
99101
for (const line of refs.split('\n')) {
100102
const trimmed = line.trim()
101103
if (!trimmed) continue
@@ -104,7 +106,10 @@ async function importBundle(fullPath: string, bundlePath: string, branch: string
104106
const name = trimmed.slice(0, firstSpace)
105107
if (name === 'HEAD') continue
106108
const sha = trimmed.slice(firstSpace + 1)
107-
await gitRaw(fullPath, ['update-ref', `refs/heads/${name}`, sha])
109+
updates.push(`update refs/heads/${name} ${sha}\n`)
110+
}
111+
if (updates.length > 0) {
112+
await gitRaw(fullPath, ['update-ref', '--stdin'], process.env, updates.join(''))
108113
}
109114

110115
if (branch) {
@@ -113,17 +118,15 @@ async function importBundle(fullPath: string, bundlePath: string, branch: string
113118
if (head) await gitRaw(fullPath, ['reset', '--hard', head])
114119
}
115120

116-
await gitRaw(fullPath, ['for-each-ref', '--format=%(refname)', 'refs/remotes/ocm-sync'])
117-
.then(async (out) => {
118-
for (const ref of out.split('\n').map((line) => line.trim()).filter(Boolean)) {
119-
await gitRaw(fullPath, ['update-ref', '-d', ref])
120-
}
121-
})
122-
.catch(() => {})
121+
const syncRefsOut = await gitRaw(fullPath, ['for-each-ref', '--format=%(refname)', 'refs/remotes/ocm-sync']).catch(() => '')
122+
const deletes = syncRefsOut.split('\n').map((l) => l.trim()).filter(Boolean).map((ref) => `delete ${ref}\n`)
123+
if (deletes.length > 0) {
124+
await gitRaw(fullPath, ['update-ref', '--stdin'], process.env, deletes.join('')).catch(() => {})
125+
}
123126
}
124127

125128
async function createBundle(fullPath: string): Promise<string> {
126-
const stagingRoot = join(getReposPath(), '.ocm-staging')
129+
const stagingRoot = getStagingRoot()
127130
mkdirSync(stagingRoot, { recursive: true })
128131
const bundleDir = mkdtempSync(join(stagingRoot, 'bundle-'))
129132
const bundlePath = join(bundleDir, 'repo.bundle')
@@ -338,7 +341,7 @@ export function createInternalRepoMirrorRoutes(db: Database) {
338341
const rawBody = c.req.raw.body
339342
if (!rawBody) return c.json({ error: 'no body provided' }, 400)
340343

341-
const stagingRoot = join(getReposPath(), '.ocm-staging')
344+
const stagingRoot = getStagingRoot()
342345
mkdirSync(stagingRoot, { recursive: true })
343346
const bundleDir = mkdtempSync(join(stagingRoot, 'bundle-upload-'))
344347
const bundlePath = join(bundleDir, 'repo.bundle')
@@ -460,7 +463,7 @@ export function createInternalRepoMirrorRoutes(db: Database) {
460463
try {
461464
const ignored = await gitOut(fullPath, ['ls-files', '--others', '--ignored', '--exclude-standard', '--directory'])
462465
if (ignored.trim()) {
463-
const excludeParent = join(getReposPath(), '.ocm-staging')
466+
const excludeParent = getStagingRoot()
464467
mkdirSync(excludeParent, { recursive: true })
465468
ignoreFile = mkdtempSync(join(excludeParent, 'exclude-'))
466469
writeFileSync(join(ignoreFile, '.gitignore'), ignored)

backend/src/routes/settings.ts

Lines changed: 10 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,7 @@ import {
2525
} from '@opencode-manager/shared'
2626
import { logger } from '../utils/logger'
2727
import {
28-
fetchAvailableModels,
29-
generateDiscoveryCacheKey,
30-
getCachedDiscovery,
31-
cacheDiscovery,
32-
ensureDiscoveryCacheDir,
28+
discoverModelsCached,
3329
} from '../utils/discovery-cache'
3430
import { opencodeServerManager, ConfigReloadError } from '../services/opencode-single-server'
3531
import { getOrCreateInternalToken, rotateInternalToken } from '../services/internal-token'
@@ -1904,22 +1900,17 @@ export function createSettingsRoutes(db: Database, gitAuthService: GitAuthServic
19041900
}
19051901

19061902
const trimmedBaseUrl = baseUrl.trim()
1907-
const cacheKey = generateDiscoveryCacheKey(trimmedBaseUrl, apiKey, 'opencode-models')
19081903

1909-
if (!forceRefresh) {
1910-
const cachedModels = await getCachedDiscovery<string[]>(cacheKey)
1911-
if (cachedModels) {
1912-
return c.json({ models: cachedModels, cached: true })
1913-
}
1914-
}
1915-
1916-
await ensureDiscoveryCacheDir()
1917-
logger.info(`Discovering OpenCode models from ${trimmedBaseUrl}`)
1918-
1919-
const models = await fetchAvailableModels(trimmedBaseUrl, apiKey, /.*/, [])
1920-
await cacheDiscovery(cacheKey, models)
1904+
const { models, cached } = await discoverModelsCached({
1905+
baseUrl: trimmedBaseUrl,
1906+
apiKey,
1907+
type: 'opencode-models',
1908+
filterPattern: /.*/,
1909+
defaultModels: [],
1910+
forceRefresh,
1911+
})
19211912

1922-
return c.json({ models, cached: false })
1913+
return c.json({ models, cached })
19231914
} catch (error) {
19241915
logger.error('Failed to discover OpenCode models:', error)
19251916
return c.json({ error: 'Failed to discover models' }, 500)

backend/src/routes/stt.ts

Lines changed: 18 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,7 @@ import { SettingsService } from '../services/settings'
44
import { logger } from '../utils/logger'
55
import {
66
normalizeToBaseUrl,
7-
ensureDiscoveryCacheDir,
8-
getCachedDiscovery,
9-
cacheDiscovery,
10-
generateDiscoveryCacheKey,
11-
fetchAvailableModels,
7+
discoverModelsCached,
128
} from '../utils/discovery-cache'
139
import { type STTConfig } from '@opencode-manager/shared'
1410

@@ -142,37 +138,26 @@ export function createSTTRoutes(db: Database) {
142138
return c.json({ error: 'STT not configured' }, 400)
143139
}
144140

145-
const cacheKey = generateDiscoveryCacheKey(sttConfig.endpoint, sttConfig.apiKey, 'models')
141+
const { models, cached } = await discoverModelsCached({
142+
baseUrl: sttConfig.endpoint,
143+
apiKey: sttConfig.apiKey,
144+
type: 'models',
145+
filterPattern: /whisper|transcri/,
146+
defaultModels: ['whisper-1'],
147+
forceRefresh,
148+
})
146149

147-
if (!forceRefresh) {
148-
const cachedModels = await getCachedDiscovery<string[]>(cacheKey)
149-
if (cachedModels) {
150-
logger.info(`STT models cache hit for user ${userId}`)
151-
return c.json({ models: cachedModels, cached: true })
152-
}
150+
if (!cached) {
151+
await settingsService.updateSettings({
152+
stt: {
153+
...sttConfig,
154+
availableModels: models,
155+
lastModelsFetch: Date.now(),
156+
} as STTConfig,
157+
}, userId)
153158
}
154159

155-
await ensureDiscoveryCacheDir()
156-
logger.info(`Fetching STT models for user ${userId}`)
157-
158-
const models = await fetchAvailableModels(
159-
sttConfig.endpoint,
160-
sttConfig.apiKey,
161-
/whisper|transcri/,
162-
['whisper-1'],
163-
)
164-
await cacheDiscovery(cacheKey, models)
165-
166-
await settingsService.updateSettings({
167-
stt: {
168-
...sttConfig,
169-
availableModels: models,
170-
lastModelsFetch: Date.now()
171-
} as STTConfig
172-
}, userId)
173-
174-
logger.info(`Fetched ${models.length} STT models`)
175-
return c.json({ models, cached: false })
160+
return c.json({ models, cached })
176161
} catch (error) {
177162
logger.error('Failed to fetch STT models:', error)
178163
return c.json({ error: 'Failed to fetch models' }, 500)

backend/src/routes/tts.ts

Lines changed: 39 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,8 @@ import { logger } from '../utils/logger'
99
import { getWorkspacePath } from '@opencode-manager/shared/config/env'
1010
import {
1111
normalizeToBaseUrl,
12-
ensureDiscoveryCacheDir,
13-
getCachedDiscovery,
14-
cacheDiscovery,
15-
generateDiscoveryCacheKey,
16-
fetchAvailableModels,
12+
discoverModelsCached,
13+
discoverCached,
1714
} from '../utils/discovery-cache'
1815

1916
const TTS_CACHE_DIR = join(getWorkspacePath(), 'cache', 'tts')
@@ -357,40 +354,26 @@ export function createTTSRoutes(db: Database) {
357354
return c.json({ error: 'TTS not configured' }, 400)
358355
}
359356

360-
const cacheKey = generateDiscoveryCacheKey(ttsConfig.endpoint, ttsConfig.apiKey, 'models')
361-
362-
// Check cache first (unless force refresh)
363-
if (!forceRefresh) {
364-
const cachedModels = await getCachedDiscovery<string[]>(cacheKey)
365-
if (cachedModels) {
366-
logger.info(`Models cache hit for user ${userId}`)
367-
return c.json({ models: cachedModels, cached: true })
368-
}
357+
const { models, cached } = await discoverModelsCached({
358+
baseUrl: ttsConfig.endpoint,
359+
apiKey: ttsConfig.apiKey,
360+
type: 'models',
361+
filterPattern: /tts|audio|speech/,
362+
defaultModels: ['tts-1', 'tts-1-hd'],
363+
forceRefresh,
364+
})
365+
366+
if (!cached) {
367+
await settingsService.updateSettings({
368+
tts: {
369+
...ttsConfig,
370+
availableModels: models,
371+
lastModelsFetch: Date.now(),
372+
},
373+
}, userId)
369374
}
370-
371-
// Fetch from API
372-
await ensureDiscoveryCacheDir()
373-
logger.info(`Fetching TTS models for user ${userId}`)
374-
375-
const models = await fetchAvailableModels(
376-
ttsConfig.endpoint,
377-
ttsConfig.apiKey,
378-
/tts|audio|speech/,
379-
['tts-1', 'tts-1-hd'],
380-
)
381-
await cacheDiscovery(cacheKey, models)
382-
383-
// Update user preferences with available models
384-
await settingsService.updateSettings({
385-
tts: {
386-
...ttsConfig,
387-
availableModels: models,
388-
lastModelsFetch: Date.now()
389-
}
390-
}, userId)
391-
392-
logger.info(`Fetched ${models.length} TTS models`)
393-
return c.json({ models, cached: false })
375+
376+
return c.json({ models, cached })
394377
} catch (error) {
395378
logger.error('Failed to fetch TTS models:', error)
396379
return c.json({ error: 'Failed to fetch models' }, 500)
@@ -410,35 +393,25 @@ export function createTTSRoutes(db: Database) {
410393
return c.json({ error: 'TTS not configured' }, 400)
411394
}
412395

413-
const cacheKey = generateDiscoveryCacheKey(ttsConfig.endpoint, ttsConfig.apiKey, 'voices')
414-
415-
// Check cache first (unless force refresh)
416-
if (!forceRefresh) {
417-
const cachedVoices = await getCachedDiscovery<string[]>(cacheKey)
418-
if (cachedVoices) {
419-
logger.info(`Voices cache hit for user ${userId}`)
420-
return c.json({ voices: cachedVoices, cached: true })
421-
}
396+
const { value: voices, cached } = await discoverCached({
397+
baseUrl: ttsConfig.endpoint,
398+
apiKey: ttsConfig.apiKey,
399+
type: 'voices',
400+
forceRefresh,
401+
fetcher: () => fetchAvailableVoices(ttsConfig.endpoint, ttsConfig.apiKey),
402+
})
403+
404+
if (!cached) {
405+
await settingsService.updateSettings({
406+
tts: {
407+
...ttsConfig,
408+
availableVoices: voices,
409+
lastVoicesFetch: Date.now(),
410+
},
411+
}, userId)
422412
}
423-
424-
// Fetch from API
425-
await ensureDiscoveryCacheDir()
426-
logger.info(`Fetching TTS voices for user ${userId}`)
427-
428-
const voices = await fetchAvailableVoices(ttsConfig.endpoint, ttsConfig.apiKey)
429-
await cacheDiscovery(cacheKey, voices)
430-
431-
// Update user preferences with available voices
432-
await settingsService.updateSettings({
433-
tts: {
434-
...ttsConfig,
435-
availableVoices: voices,
436-
lastVoicesFetch: Date.now()
437-
}
438-
}, userId)
439-
440-
logger.info(`Fetched ${voices.length} TTS voices`)
441-
return c.json({ voices, cached: false })
413+
414+
return c.json({ voices, cached })
442415
} catch (error) {
443416
logger.error('Failed to fetch TTS voices:', error)
444417
return c.json({ error: 'Failed to fetch voices' }, 500)

0 commit comments

Comments
 (0)