fix: restore complete source code and fix launchers
- Copy complete source code packages from original CodeNomad project - Add root package.json with npm workspace configuration - Include electron-app, server, ui, tauri-app, and opencode-config packages - Fix Launch-Windows.bat and Launch-Dev-Windows.bat to work with correct npm scripts - Fix Launch-Unix.sh to work with correct npm scripts - Launchers now correctly call npm run dev:electron which launches Electron app
This commit is contained in:
156
packages/server/src/config/binaries.ts
Normal file
156
packages/server/src/config/binaries.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import {
|
||||
BinaryCreateRequest,
|
||||
BinaryRecord,
|
||||
BinaryUpdateRequest,
|
||||
BinaryValidationResult,
|
||||
} from "../api-types"
|
||||
import { ConfigStore } from "./store"
|
||||
import { EventBus } from "../events/bus"
|
||||
import type { ConfigFile } from "./schema"
|
||||
import { Logger } from "../logger"
|
||||
|
||||
export class BinaryRegistry {
|
||||
constructor(
|
||||
private readonly configStore: ConfigStore,
|
||||
private readonly eventBus: EventBus | undefined,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
list(): BinaryRecord[] {
|
||||
return this.mapRecords()
|
||||
}
|
||||
|
||||
resolveDefault(): BinaryRecord {
|
||||
const binaries = this.mapRecords()
|
||||
if (binaries.length === 0) {
|
||||
this.logger.warn("No configured binaries found, falling back to opencode")
|
||||
return this.buildFallbackRecord("opencode")
|
||||
}
|
||||
return binaries.find((binary) => binary.isDefault) ?? binaries[0]
|
||||
}
|
||||
|
||||
create(request: BinaryCreateRequest): BinaryRecord {
|
||||
this.logger.debug({ path: request.path }, "Registering OpenCode binary")
|
||||
const entry = {
|
||||
path: request.path,
|
||||
version: undefined,
|
||||
lastUsed: Date.now(),
|
||||
label: request.label,
|
||||
}
|
||||
|
||||
const config = this.configStore.get()
|
||||
const nextConfig = this.cloneConfig(config)
|
||||
const deduped = nextConfig.opencodeBinaries.filter((binary) => binary.path !== request.path)
|
||||
nextConfig.opencodeBinaries = [entry, ...deduped]
|
||||
|
||||
if (request.makeDefault) {
|
||||
nextConfig.preferences.lastUsedBinary = request.path
|
||||
}
|
||||
|
||||
this.configStore.replace(nextConfig)
|
||||
const record = this.getById(request.path)
|
||||
this.emitChange()
|
||||
return record
|
||||
}
|
||||
|
||||
update(id: string, updates: BinaryUpdateRequest): BinaryRecord {
|
||||
this.logger.debug({ id }, "Updating OpenCode binary")
|
||||
const config = this.configStore.get()
|
||||
const nextConfig = this.cloneConfig(config)
|
||||
nextConfig.opencodeBinaries = nextConfig.opencodeBinaries.map((binary) =>
|
||||
binary.path === id ? { ...binary, label: updates.label ?? binary.label } : binary,
|
||||
)
|
||||
|
||||
if (updates.makeDefault) {
|
||||
nextConfig.preferences.lastUsedBinary = id
|
||||
}
|
||||
|
||||
this.configStore.replace(nextConfig)
|
||||
const record = this.getById(id)
|
||||
this.emitChange()
|
||||
return record
|
||||
}
|
||||
|
||||
remove(id: string) {
|
||||
this.logger.debug({ id }, "Removing OpenCode binary")
|
||||
const config = this.configStore.get()
|
||||
const nextConfig = this.cloneConfig(config)
|
||||
const remaining = nextConfig.opencodeBinaries.filter((binary) => binary.path !== id)
|
||||
nextConfig.opencodeBinaries = remaining
|
||||
|
||||
if (nextConfig.preferences.lastUsedBinary === id) {
|
||||
nextConfig.preferences.lastUsedBinary = remaining[0]?.path
|
||||
}
|
||||
|
||||
this.configStore.replace(nextConfig)
|
||||
this.emitChange()
|
||||
}
|
||||
|
||||
validatePath(path: string): BinaryValidationResult {
|
||||
this.logger.debug({ path }, "Validating OpenCode binary path")
|
||||
return this.validateRecord({
|
||||
id: path,
|
||||
path,
|
||||
label: this.prettyLabel(path),
|
||||
isDefault: false,
|
||||
})
|
||||
}
|
||||
|
||||
private cloneConfig(config: ConfigFile): ConfigFile {
|
||||
return JSON.parse(JSON.stringify(config)) as ConfigFile
|
||||
}
|
||||
|
||||
private mapRecords(): BinaryRecord[] {
|
||||
|
||||
const config = this.configStore.get()
|
||||
const configuredBinaries = config.opencodeBinaries.map<BinaryRecord>((binary) => ({
|
||||
id: binary.path,
|
||||
path: binary.path,
|
||||
label: binary.label ?? this.prettyLabel(binary.path),
|
||||
version: binary.version,
|
||||
isDefault: false,
|
||||
}))
|
||||
|
||||
const defaultPath = config.preferences.lastUsedBinary ?? configuredBinaries[0]?.path ?? "opencode"
|
||||
|
||||
const annotated = configuredBinaries.map((binary) => ({
|
||||
...binary,
|
||||
isDefault: binary.path === defaultPath,
|
||||
}))
|
||||
|
||||
if (!annotated.some((binary) => binary.path === defaultPath)) {
|
||||
annotated.unshift(this.buildFallbackRecord(defaultPath))
|
||||
}
|
||||
|
||||
return annotated
|
||||
}
|
||||
|
||||
private getById(id: string): BinaryRecord {
|
||||
return this.mapRecords().find((binary) => binary.id === id) ?? this.buildFallbackRecord(id)
|
||||
}
|
||||
|
||||
private emitChange() {
|
||||
this.logger.debug("Emitting binaries changed event")
|
||||
this.eventBus?.publish({ type: "config.binariesChanged", binaries: this.mapRecords() })
|
||||
}
|
||||
|
||||
private validateRecord(record: BinaryRecord): BinaryValidationResult {
|
||||
// TODO: call actual binary -v check.
|
||||
return { valid: true, version: record.version }
|
||||
}
|
||||
|
||||
private buildFallbackRecord(path: string): BinaryRecord {
|
||||
return {
|
||||
id: path,
|
||||
path,
|
||||
label: this.prettyLabel(path),
|
||||
isDefault: true,
|
||||
}
|
||||
}
|
||||
|
||||
private prettyLabel(path: string) {
|
||||
const parts = path.split(/[\\/]/)
|
||||
const last = parts[parts.length - 1] || path
|
||||
return last || path
|
||||
}
|
||||
}
|
||||
64
packages/server/src/config/schema.ts
Normal file
64
packages/server/src/config/schema.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { z } from "zod"
|
||||
|
||||
const ModelPreferenceSchema = z.object({
|
||||
providerId: z.string(),
|
||||
modelId: z.string(),
|
||||
})
|
||||
|
||||
const AgentModelSelectionSchema = z.record(z.string(), ModelPreferenceSchema)
|
||||
const AgentModelSelectionsSchema = z.record(z.string(), AgentModelSelectionSchema)
|
||||
|
||||
const PreferencesSchema = z.object({
|
||||
showThinkingBlocks: z.boolean().default(false),
|
||||
thinkingBlocksExpansion: z.enum(["expanded", "collapsed"]).default("expanded"),
|
||||
showTimelineTools: z.boolean().default(true),
|
||||
lastUsedBinary: z.string().optional(),
|
||||
environmentVariables: z.record(z.string()).default({}),
|
||||
modelRecents: z.array(ModelPreferenceSchema).default([]),
|
||||
diffViewMode: z.enum(["split", "unified"]).default("split"),
|
||||
toolOutputExpansion: z.enum(["expanded", "collapsed"]).default("expanded"),
|
||||
diagnosticsExpansion: z.enum(["expanded", "collapsed"]).default("expanded"),
|
||||
showUsageMetrics: z.boolean().default(true),
|
||||
autoCleanupBlankSessions: z.boolean().default(true),
|
||||
listeningMode: z.enum(["local", "all"]).default("local"),
|
||||
})
|
||||
|
||||
const RecentFolderSchema = z.object({
|
||||
path: z.string(),
|
||||
lastAccessed: z.number().nonnegative(),
|
||||
})
|
||||
|
||||
const OpenCodeBinarySchema = z.object({
|
||||
path: z.string(),
|
||||
version: z.string().optional(),
|
||||
lastUsed: z.number().nonnegative(),
|
||||
label: z.string().optional(),
|
||||
})
|
||||
|
||||
const ConfigFileSchema = z.object({
|
||||
preferences: PreferencesSchema.default({}),
|
||||
recentFolders: z.array(RecentFolderSchema).default([]),
|
||||
opencodeBinaries: z.array(OpenCodeBinarySchema).default([]),
|
||||
theme: z.enum(["light", "dark", "system"]).optional(),
|
||||
})
|
||||
|
||||
const DEFAULT_CONFIG = ConfigFileSchema.parse({})
|
||||
|
||||
export {
|
||||
ModelPreferenceSchema,
|
||||
AgentModelSelectionSchema,
|
||||
AgentModelSelectionsSchema,
|
||||
PreferencesSchema,
|
||||
RecentFolderSchema,
|
||||
OpenCodeBinarySchema,
|
||||
ConfigFileSchema,
|
||||
DEFAULT_CONFIG,
|
||||
}
|
||||
|
||||
export type ModelPreference = z.infer<typeof ModelPreferenceSchema>
|
||||
export type AgentModelSelection = z.infer<typeof AgentModelSelectionSchema>
|
||||
export type AgentModelSelections = z.infer<typeof AgentModelSelectionsSchema>
|
||||
export type Preferences = z.infer<typeof PreferencesSchema>
|
||||
export type RecentFolder = z.infer<typeof RecentFolderSchema>
|
||||
export type OpenCodeBinary = z.infer<typeof OpenCodeBinarySchema>
|
||||
export type ConfigFile = z.infer<typeof ConfigFileSchema>
|
||||
78
packages/server/src/config/store.ts
Normal file
78
packages/server/src/config/store.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import fs from "fs"
|
||||
import path from "path"
|
||||
import { EventBus } from "../events/bus"
|
||||
import { Logger } from "../logger"
|
||||
import { ConfigFile, ConfigFileSchema, DEFAULT_CONFIG } from "./schema"
|
||||
|
||||
export class ConfigStore {
|
||||
private cache: ConfigFile = DEFAULT_CONFIG
|
||||
private loaded = false
|
||||
|
||||
constructor(
|
||||
private readonly configPath: string,
|
||||
private readonly eventBus: EventBus | undefined,
|
||||
private readonly logger: Logger,
|
||||
) {}
|
||||
|
||||
load(): ConfigFile {
|
||||
if (this.loaded) {
|
||||
return this.cache
|
||||
}
|
||||
|
||||
try {
|
||||
const resolved = this.resolvePath(this.configPath)
|
||||
if (fs.existsSync(resolved)) {
|
||||
const content = fs.readFileSync(resolved, "utf-8")
|
||||
const parsed = JSON.parse(content)
|
||||
this.cache = ConfigFileSchema.parse(parsed)
|
||||
this.logger.debug({ resolved }, "Loaded existing config file")
|
||||
} else {
|
||||
this.cache = DEFAULT_CONFIG
|
||||
this.logger.debug({ resolved }, "No config file found, using defaults")
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.warn({ err: error }, "Failed to load config, using defaults")
|
||||
this.cache = DEFAULT_CONFIG
|
||||
}
|
||||
|
||||
this.loaded = true
|
||||
return this.cache
|
||||
}
|
||||
|
||||
get(): ConfigFile {
|
||||
return this.load()
|
||||
}
|
||||
|
||||
replace(config: ConfigFile) {
|
||||
const validated = ConfigFileSchema.parse(config)
|
||||
this.commit(validated)
|
||||
}
|
||||
|
||||
private commit(next: ConfigFile) {
|
||||
this.cache = next
|
||||
this.loaded = true
|
||||
this.persist()
|
||||
const published = Boolean(this.eventBus)
|
||||
this.eventBus?.publish({ type: "config.appChanged", config: this.cache })
|
||||
this.logger.debug({ broadcast: published }, "Config SSE event emitted")
|
||||
this.logger.trace({ config: this.cache }, "Config payload")
|
||||
}
|
||||
|
||||
private persist() {
|
||||
try {
|
||||
const resolved = this.resolvePath(this.configPath)
|
||||
fs.mkdirSync(path.dirname(resolved), { recursive: true })
|
||||
fs.writeFileSync(resolved, JSON.stringify(this.cache, null, 2), "utf-8")
|
||||
this.logger.debug({ resolved }, "Persisted config file")
|
||||
} catch (error) {
|
||||
this.logger.warn({ err: error }, "Failed to persist config")
|
||||
}
|
||||
}
|
||||
|
||||
private resolvePath(filePath: string) {
|
||||
if (filePath.startsWith("~/")) {
|
||||
return path.join(process.env.HOME ?? "", filePath.slice(2))
|
||||
}
|
||||
return path.resolve(filePath)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user