opencode-provider-switch/.opencode/lib/trellis-context.js
2026-04-16 23:46:01 +08:00

233 lines
5.6 KiB
JavaScript

/**
* Trellis Context Manager
*
* Utility class for OpenCode plugins providing file reading,
* JSONL parsing, and context building capabilities.
*/
import { existsSync, readFileSync, appendFileSync, readdirSync } from "fs"
import { isAbsolute, join } from "path"
import { platform } from "os"
import { execSync } from "child_process"
const PYTHON_CMD = platform() === "win32" ? "python" : "python3"
// Debug logging
const DEBUG_LOG = "/tmp/trellis-plugin-debug.log"
function debugLog(prefix, ...args) {
const timestamp = new Date().toISOString()
const msg = `[${timestamp}] [${prefix}] ${args.map(a => typeof a === "object" ? JSON.stringify(a) : a).join(" ")}\n`
try {
appendFileSync(DEBUG_LOG, msg)
} catch {
// ignore
}
}
/**
* Trellis Context Manager
*/
export class TrellisContext {
constructor(directory) {
this.directory = directory
debugLog("context", "TrellisContext initialized", { directory })
}
// ============================================================
// Trellis Project Detection
// ============================================================
isTrellisProject() {
return existsSync(join(this.directory, ".trellis"))
}
/**
* Get current task directory from .trellis/.current-task
*/
getCurrentTask() {
try {
const currentTaskPath = join(this.directory, ".trellis", ".current-task")
if (!existsSync(currentTaskPath)) {
return null
}
const taskRef = readFileSync(currentTaskPath, "utf-8").trim()
const normalized = this.normalizeTaskRef(taskRef)
return normalized || null
} catch {
return null
}
}
normalizeTaskRef(taskRef) {
if (!taskRef) {
return ""
}
if (isAbsolute(taskRef)) {
return taskRef.trim()
}
let normalized = taskRef.trim().replace(/\\/g, "/")
while (normalized.startsWith("./")) {
normalized = normalized.slice(2)
}
if (normalized.startsWith("tasks/")) {
return `.trellis/${normalized}`
}
return normalized
}
resolveTaskDir(taskRef) {
const normalized = this.normalizeTaskRef(taskRef)
if (!normalized) {
return null
}
if (isAbsolute(normalized)) {
return normalized
}
if (normalized.startsWith(".trellis/")) {
return join(this.directory, normalized)
}
return join(this.directory, ".trellis", "tasks", normalized)
}
// ============================================================
// File Reading Utilities
// ============================================================
readFile(filePath) {
try {
if (existsSync(filePath)) {
return readFileSync(filePath, "utf-8")
}
} catch {
// Ignore read errors
}
return null
}
readProjectFile(relativePath) {
return this.readFile(join(this.directory, relativePath))
}
runScript(scriptPath, cwd = null) {
try {
const result = execSync(`${PYTHON_CMD} "${scriptPath}"`, {
cwd: cwd || this.directory,
timeout: 10000,
encoding: "utf-8",
stdio: ["pipe", "pipe", "pipe"]
})
return result || ""
} catch {
return ""
}
}
// ============================================================
// JSONL Reading
// ============================================================
readDirectoryMdFiles(dirPath, maxFiles = 20) {
const results = []
const fullPath = join(this.directory, dirPath)
if (!existsSync(fullPath)) {
return results
}
try {
const files = readdirSync(fullPath)
.filter(f => f.endsWith(".md"))
.sort()
.slice(0, maxFiles)
for (const filename of files) {
const filePath = join(dirPath, filename)
const content = this.readProjectFile(filePath)
if (content) {
results.push({ path: filePath, content })
}
}
} catch {
// Ignore directory read errors
}
return results
}
/**
* Read a JSONL file and load referenced files/directories
* Supports:
* {"file": "path/to/file.md", "reason": "..."}
* {"file": "path/to/dir/", "type": "directory", "reason": "..."}
*/
readJsonlWithFiles(jsonlPath) {
const results = []
const content = this.readFile(jsonlPath)
if (!content) return results
for (const line of content.split("\n")) {
if (!line.trim()) continue
try {
const item = JSON.parse(line)
const file = item.file || item.path
const entryType = item.type || "file"
if (!file) continue
if (entryType === "directory") {
const dirEntries = this.readDirectoryMdFiles(file)
results.push(...dirEntries)
} else {
const fullPath = join(this.directory, file)
const fileContent = this.readFile(fullPath)
if (fileContent) {
results.push({ path: file, content: fileContent })
}
}
} catch {
// Ignore parse errors for individual lines
}
}
return results
}
buildContextFromEntries(entries) {
return entries.map(e => `=== ${e.path} ===\n${e.content}`).join("\n\n")
}
}
// ============================================================
// Context Collector (for session deduplication)
// ============================================================
class ContextCollector {
constructor() {
this.processed = new Set()
}
markProcessed(sessionID) {
this.processed.add(sessionID)
}
isProcessed(sessionID) {
return this.processed.has(sessionID)
}
clear(sessionID) {
this.processed.delete(sessionID)
}
}
// Singleton instance
export const contextCollector = new ContextCollector()
// Export debug log for plugins
export { debugLog }