1a734adb4d
Moves effect logging, observability, runtime utilities, flags, installation version info, and process utilities from opencode to core package. This enables better code sharing across packages and establishes core as the single source of truth for foundational utilities. All internal imports updated to use @opencode-ai/core paths for consistency.
161 lines
4.7 KiB
TypeScript
161 lines
4.7 KiB
TypeScript
import type { Argv } from "yargs"
|
|
import { cmd } from "./cmd"
|
|
import { Session } from "../../session"
|
|
import { SessionID } from "../../session/schema"
|
|
import { bootstrap } from "../bootstrap"
|
|
import { UI } from "../ui"
|
|
import { Locale } from "../../util"
|
|
import { Flag } from "@opencode-ai/core/flag/flag"
|
|
import { Filesystem } from "../../util"
|
|
import { Process } from "../../util"
|
|
import { EOL } from "os"
|
|
import path from "path"
|
|
import { which } from "../../util/which"
|
|
import { AppRuntime } from "@/effect/app-runtime"
|
|
|
|
function pagerCmd(): string[] {
|
|
const lessOptions = ["-R", "-S"]
|
|
if (process.platform !== "win32") {
|
|
return ["less", ...lessOptions]
|
|
}
|
|
|
|
// user could have less installed via other options
|
|
const lessOnPath = which("less")
|
|
if (lessOnPath) {
|
|
if (Filesystem.stat(lessOnPath)?.size) return [lessOnPath, ...lessOptions]
|
|
}
|
|
|
|
if (Flag.OPENCODE_GIT_BASH_PATH) {
|
|
const less = path.join(Flag.OPENCODE_GIT_BASH_PATH, "..", "..", "usr", "bin", "less.exe")
|
|
if (Filesystem.stat(less)?.size) return [less, ...lessOptions]
|
|
}
|
|
|
|
const git = which("git")
|
|
if (git) {
|
|
const less = path.join(git, "..", "..", "usr", "bin", "less.exe")
|
|
if (Filesystem.stat(less)?.size) return [less, ...lessOptions]
|
|
}
|
|
|
|
// Fall back to Windows built-in more (via cmd.exe)
|
|
return ["cmd", "/c", "more"]
|
|
}
|
|
|
|
export const SessionCommand = cmd({
|
|
command: "session",
|
|
describe: "manage sessions",
|
|
builder: (yargs: Argv) => yargs.command(SessionListCommand).command(SessionDeleteCommand).demandCommand(),
|
|
async handler() {},
|
|
})
|
|
|
|
export const SessionDeleteCommand = cmd({
|
|
command: "delete <sessionID>",
|
|
describe: "delete a session",
|
|
builder: (yargs: Argv) => {
|
|
return yargs.positional("sessionID", {
|
|
describe: "session ID to delete",
|
|
type: "string",
|
|
demandOption: true,
|
|
})
|
|
},
|
|
handler: async (args) => {
|
|
await bootstrap(process.cwd(), async () => {
|
|
const sessionID = SessionID.make(args.sessionID)
|
|
try {
|
|
await AppRuntime.runPromise(Session.Service.use((svc) => svc.get(sessionID)))
|
|
} catch {
|
|
UI.error(`Session not found: ${args.sessionID}`)
|
|
process.exit(1)
|
|
}
|
|
await AppRuntime.runPromise(Session.Service.use((svc) => svc.remove(sessionID)))
|
|
UI.println(UI.Style.TEXT_SUCCESS_BOLD + `Session ${args.sessionID} deleted` + UI.Style.TEXT_NORMAL)
|
|
})
|
|
},
|
|
})
|
|
|
|
export const SessionListCommand = cmd({
|
|
command: "list",
|
|
describe: "list sessions",
|
|
builder: (yargs: Argv) => {
|
|
return yargs
|
|
.option("max-count", {
|
|
alias: "n",
|
|
describe: "limit to N most recent sessions",
|
|
type: "number",
|
|
})
|
|
.option("format", {
|
|
describe: "output format",
|
|
type: "string",
|
|
choices: ["table", "json"],
|
|
default: "table",
|
|
})
|
|
},
|
|
handler: async (args) => {
|
|
await bootstrap(process.cwd(), async () => {
|
|
const sessions = [...Session.list({ roots: true, limit: args.maxCount })]
|
|
|
|
if (sessions.length === 0) {
|
|
return
|
|
}
|
|
|
|
let output: string
|
|
if (args.format === "json") {
|
|
output = formatSessionJSON(sessions)
|
|
} else {
|
|
output = formatSessionTable(sessions)
|
|
}
|
|
|
|
const shouldPaginate = process.stdout.isTTY && !args.maxCount && args.format === "table"
|
|
|
|
if (shouldPaginate) {
|
|
const proc = Process.spawn(pagerCmd(), {
|
|
stdin: "pipe",
|
|
stdout: "inherit",
|
|
stderr: "inherit",
|
|
})
|
|
|
|
if (!proc.stdin) {
|
|
console.log(output)
|
|
return
|
|
}
|
|
|
|
proc.stdin.write(output)
|
|
proc.stdin.end()
|
|
await proc.exited
|
|
} else {
|
|
console.log(output)
|
|
}
|
|
})
|
|
},
|
|
})
|
|
|
|
function formatSessionTable(sessions: Session.Info[]): string {
|
|
const lines: string[] = []
|
|
|
|
const maxIdWidth = Math.max(20, ...sessions.map((s) => s.id.length))
|
|
const maxTitleWidth = Math.max(25, ...sessions.map((s) => s.title.length))
|
|
|
|
const header = `Session ID${" ".repeat(maxIdWidth - 10)} Title${" ".repeat(maxTitleWidth - 5)} Updated`
|
|
lines.push(header)
|
|
lines.push("─".repeat(header.length))
|
|
for (const session of sessions) {
|
|
const truncatedTitle = Locale.truncate(session.title, maxTitleWidth)
|
|
const timeStr = Locale.todayTimeOrDateTime(session.time.updated)
|
|
const line = `${session.id.padEnd(maxIdWidth)} ${truncatedTitle.padEnd(maxTitleWidth)} ${timeStr}`
|
|
lines.push(line)
|
|
}
|
|
|
|
return lines.join(EOL)
|
|
}
|
|
|
|
function formatSessionJSON(sessions: Session.Info[]): string {
|
|
const jsonData = sessions.map((session) => ({
|
|
id: session.id,
|
|
title: session.title,
|
|
updated: session.time.updated,
|
|
created: session.time.created,
|
|
projectId: session.projectID,
|
|
directory: session.directory,
|
|
}))
|
|
return JSON.stringify(jsonData, null, 2)
|
|
}
|