← Public packages
@kentcdodds/codemod-runner
Run codemods over your own saved packages: scan, dry-run with real publish checks and diffs, apply, and revert.
src/sessions.ts
173 lines · 4.7 KB · TypeScriptimport { kody } from 'kody:runtime'
import { mapPool } from './async-pool.ts'
export type SessionInfo = {
id: string
base_commit: string
published_commit: string | null
}
/**
* Opens a fresh throwaway session for a package. Omitting `conversation_id`
* guarantees a brand-new session, so the runner can never resume (or
* clobber) an editing session the user has in flight. The user's own
* sessions live in their own overlays; unpublished pushed work surfaces as
* base/published drift and is skipped by callers.
*/
export async function openFreshSession(input: {
packageId: string
}): Promise<SessionInfo> {
const session = (await kody.repoOpenSession({
target: { kind: 'package', package_id: input.packageId },
})) as SessionInfo
return session
}
export async function discardSession(sessionId: string): Promise<void> {
try {
await kody.repoDiscardSession({ session_id: sessionId })
} catch {
// Best-effort: published sessions and expired sessions may refuse.
}
}
/**
* Enumerates the session's file paths with an every-line regex match.
* Limitation: files with zero lines (completely empty files) do not match
* and are invisible to codemods run through this runner.
*/
export async function listSessionFiles(
sessionId: string,
): Promise<Array<string>> {
const result = (await kody.repoSearch({
session_id: sessionId,
pattern: '^',
mode: 'regex',
output_mode: 'files',
limit: 5000,
})) as { files: Array<{ path: string }>; truncated: boolean }
if (result.truncated) {
throw new Error('Package has too many files for the codemod runner.')
}
return result.files.map((entry) => entry.path).sort()
}
/** Paths whose contents match a JS regex. Used to skip full-tree reads on scan. */
export async function searchSessionFiles(
sessionId: string,
pattern: string,
): Promise<Array<string>> {
const result = (await kody.repoSearch({
session_id: sessionId,
pattern,
mode: 'regex',
output_mode: 'files',
limit: 5000,
})) as { files: Array<{ path: string }>; truncated: boolean }
if (result.truncated) {
throw new Error('Package has too many matching files for the codemod runner.')
}
return result.files.map((entry) => entry.path).sort()
}
const fileReadConcurrency = 16
export async function readSessionFiles(
sessionId: string,
paths: Array<string>,
): Promise<Record<string, string>> {
const entries = await mapPool(paths, fileReadConcurrency, async (path) => {
const result = (await kody.repoReadFile({
session_id: sessionId,
path,
})) as { content: string | null }
return [path, result.content] as const
})
const files: Record<string, string> = {}
for (const [path, content] of entries) {
if (typeof content === 'string') {
files[path] = content
}
}
return files
}
export type WriteOutcome = { diff: string }
export async function writeSessionFiles(
sessionId: string,
files: Record<string, string>,
): Promise<WriteOutcome> {
const entries = Object.entries(files).map(([path, content]) => ({
path,
content,
}))
if (entries.length === 0) return { diff: '' }
const result = (await kody.repoWriteFile({
session_id: sessionId,
files: entries,
})) as { edits: Array<{ path: string; diff: string }> }
return {
diff: result.edits.map((edit) => edit.diff).join('\n'),
}
}
export async function deleteSessionFiles(
sessionId: string,
paths: Array<string>,
): Promise<void> {
if (paths.length === 0) return
for (const path of paths) {
if (/\s/.test(path)) {
throw new Error(
`Cannot delete "${path}": paths with whitespace are not supported.`,
)
}
}
await kody.repoRunCommands({
session_id: sessionId,
commands: paths.map((path) => `git rm ${path}`).join('\n'),
})
}
export type CheckOutcome = {
ok: boolean
failed: Array<string>
}
export async function runSessionChecks(
sessionId: string,
): Promise<CheckOutcome> {
const result = (await kody.repoRunChecks({
session_id: sessionId,
})) as {
ok: boolean
results: Array<{ kind: string; ok: boolean; message: string }>
}
return {
ok: result.ok,
failed: result.results
.filter((entry) => !entry.ok)
.map((entry) => `${entry.kind}: ${entry.message}`.slice(0, 500)),
}
}
export type PublishOutcome =
| { status: 'ok'; publishedCommit: string }
| { status: 'blocked'; message: string }
export async function publishSession(
sessionId: string,
): Promise<PublishOutcome> {
const result = (await kody.repoPublishSession({
session_id: sessionId,
})) as {
status: 'ok' | 'checks_outdated' | 'base_moved'
published_commit: string | null
message: string
}
if (result.status === 'ok' && result.published_commit) {
return { status: 'ok', publishedCommit: result.published_commit }
}
return { status: 'blocked', message: `${result.status}: ${result.message}` }
}