102 lines
4.1 KiB
TypeScript
102 lines
4.1 KiB
TypeScript
/**
|
|
* `dsh list-sessions` (alias `dsh ps`) — list the sessions running right now.
|
|
*
|
|
* A read-only surface: it mounts the session registry alone and never boots an
|
|
* agent tree, so listing stays fast and cannot start model work as a side
|
|
* effect. Liveness comes from the registry, which prunes records whose process
|
|
* is gone, and every displayed field including the title comes from the record,
|
|
* so no session log is opened and no backend format is assumed.
|
|
* @module @deepseek-ai/dsh/list-sessions
|
|
*/
|
|
|
|
import { Context } from 'cordis'
|
|
import { type SessionRegistryRecord } from '@deepseek-ai/dsh-session-registry'
|
|
import SessionRegistryFile from '@deepseek-ai/dsh-session-registry-file'
|
|
import { registryRoot } from './register-session.ts'
|
|
|
|
/** Column header text, also the minimum width of each column. */
|
|
const HEADERS = ['SESSION', 'PID', 'UPTIME', 'WORKSPACE', 'TITLE'] as const
|
|
|
|
/** Shown when a session has no title yet. */
|
|
const NO_TITLE = '—'
|
|
|
|
/**
|
|
* Render milliseconds of uptime as a compact human duration.
|
|
* @param ms - elapsed milliseconds since the session registered.
|
|
* @returns a short duration such as `12s`, `4m`, or `2h14m`.
|
|
*/
|
|
export function formatUptime(ms: number): string {
|
|
const seconds = Math.max(0, Math.floor(ms / 1000))
|
|
if (seconds < 60) return `${String(seconds)}s`
|
|
const minutes = Math.floor(seconds / 60)
|
|
if (minutes < 60) return `${String(minutes)}m`
|
|
const hours = Math.floor(minutes / 60)
|
|
const remainder = minutes % 60
|
|
if (hours < 24) return remainder === 0 ? `${String(hours)}h` : `${String(hours)}h${String(remainder)}m`
|
|
const days = Math.floor(hours / 24)
|
|
const leftoverHours = hours % 24
|
|
return leftoverHours === 0 ? `${String(days)}d` : `${String(days)}d${String(leftoverHours)}h`
|
|
}
|
|
|
|
/** One fully-resolved listing row, in column order. */
|
|
type Row = readonly [string, string, string, string, string]
|
|
|
|
/**
|
|
* Build the display rows for a listing, newest session first.
|
|
* @param records - the live records to render.
|
|
* @param now - the current epoch milliseconds uptime is measured against.
|
|
* @returns one row per record, each already stringified per column.
|
|
*/
|
|
export function buildRows(records: readonly SessionRegistryRecord[], now: number): Row[] {
|
|
return [...records]
|
|
.sort((left, right) => right.startedAt - left.startedAt)
|
|
.map(record => [
|
|
record.sessionId,
|
|
String(record.pid),
|
|
formatUptime(now - record.startedAt),
|
|
record.cwd,
|
|
record.title ?? NO_TITLE,
|
|
] as const)
|
|
}
|
|
|
|
/**
|
|
* Render rows as a left-aligned table with a header line.
|
|
*
|
|
* The last column is never padded, so a long title cannot add trailing
|
|
* whitespace to every line.
|
|
* @param rows - the rows to render, already stringified.
|
|
* @returns the complete table text, newline-terminated.
|
|
*/
|
|
export function renderTable(rows: readonly Row[]): string {
|
|
const widths = HEADERS.map((header, column) =>
|
|
Math.max(header.length, ...rows.map(row => row[column]?.length ?? 0)))
|
|
const line = (cells: readonly string[]): string =>
|
|
cells.map((cell, column) => column === cells.length - 1 ? cell : cell.padEnd(widths[column] ?? 0)).join(' ').trimEnd()
|
|
return [line(HEADERS), ...rows.map(row => line(row))].join('\n') + '\n'
|
|
}
|
|
|
|
/**
|
|
* List live sessions and exit. Prints a table by default, or a JSON array with
|
|
* `--json`; an empty listing is a success, not an error.
|
|
* @param json - emit the machine-readable JSON array instead of the table.
|
|
*/
|
|
export async function runListSessions(json: boolean): Promise<void> {
|
|
const ctx = new Context()
|
|
await ctx.plugin(SessionRegistryFile, { root: registryRoot() })
|
|
const records = await ctx.sessionRegistry.list()
|
|
await ctx.fiber.dispose()
|
|
|
|
if (json) {
|
|
const rows = [...records]
|
|
.sort((left, right) => right.startedAt - left.startedAt)
|
|
.map(record => ({ ...record, uptimeMs: Date.now() - record.startedAt, title: record.title ?? null }))
|
|
process.stdout.write(`${JSON.stringify(rows, undefined, 2)}\n`)
|
|
return
|
|
}
|
|
if (records.length === 0) {
|
|
process.stdout.write('no dsh sessions running\n')
|
|
return
|
|
}
|
|
process.stdout.write(renderTable(buildRows(records, Date.now())))
|
|
}
|