← 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/ledger.ts
238 lines · 6.0 KB · TypeScriptimport { packageStorage } from 'kody:runtime'
import { type CodemodFinding, type CodemodRef } from './contract.ts'
export type RunMode = 'scan' | 'dry-run' | 'apply' | 'revert'
export type ItemStatus =
| 'detected'
| 'clean'
| 'dry_run_ok'
| 'checks_failed'
| 'needs_manual'
| 'skipped_drift'
| 'skipped_unpublished'
| 'applied'
| 'reverted'
| 'failed'
export type RunItem = {
packageId: string
kodyId: string
status: ItemStatus
changedPaths: Array<string>
findings: Array<CodemodFinding>
failedChecks: Array<string>
beforeCommit: string | null
afterCommit: string | null
diff: string | null
error: string | null
}
export type StoredCodemodRef = {
kodyId: string
/** Missing only on runs created before scoped specifiers were required. */
specifier?: string
exportName: string
}
export type RunRecord = {
id: string
mode: RunMode
codemod: StoredCodemodRef | null
revertOfRunId: string | null
status: 'running' | 'completed'
createdAt: string
updatedAt: string
cursor: string | null
items: Array<RunItem>
}
export type RunSummary = {
id: string
mode: RunMode
codemod: StoredCodemodRef | null
status: 'running' | 'completed'
createdAt: string
}
const runsIndexKey = 'runs-index'
const maxIndexEntries = 100
const maxDiffChars = 16_000
const maxErrorChars = 2_000
function storage() {
return packageStorage()
}
/**
* Storage writes trigger a platform-side entitlement estimate that fans out
* to every bucket the user owns and occasionally fails transiently on large
* accounts ("bucket estimate could not be read"). Writes are retried with a
* short backoff before giving up.
*/
async function setWithRetry(key: string, value: unknown): Promise<void> {
let lastError: unknown = null
for (let attempt = 1; attempt <= 3; attempt += 1) {
try {
await storage().set(key, value)
return
} catch (error) {
lastError = error
await new Promise<void>((resolve) => {
setTimeout(resolve, 300 * attempt)
})
}
}
throw lastError instanceof Error ? lastError : new Error(String(lastError))
}
export function boundDiff(diff: string | null): string | null {
if (diff == null) return null
if (diff.length <= maxDiffChars) return diff
return `${diff.slice(0, maxDiffChars)}\n… diff truncated (${diff.length} chars total)`
}
export function boundError(message: string): string {
return message.length <= maxErrorChars
? message
: `${message.slice(0, maxErrorChars)}…`
}
export async function listRunSummaries(): Promise<Array<RunSummary>> {
const index = (await storage().get(runsIndexKey)) as Array<RunSummary> | null
return Array.isArray(index) ? index : []
}
export async function getRun(runId: string): Promise<RunRecord | null> {
const record = (await storage().get(`run:${runId}`)) as RunRecord | null
return record ?? null
}
export async function saveRun(record: RunRecord): Promise<void> {
record.updatedAt = new Date().toISOString()
await setWithRetry(`run:${record.id}`, record)
const index = await listRunSummaries()
const summary: RunSummary = {
id: record.id,
mode: record.mode,
codemod: record.codemod,
status: record.status,
createdAt: record.createdAt,
}
const next = [
summary,
...index.filter((entry) => entry.id !== record.id),
].slice(0, maxIndexEntries)
await setWithRetry(runsIndexKey, next)
}
export async function createRun(input: {
mode: RunMode
codemod: CodemodRef | null
revertOfRunId?: string | null
}): Promise<RunRecord> {
const now = new Date().toISOString()
const record: RunRecord = {
id: crypto.randomUUID(),
mode: input.mode,
codemod: input.codemod
? {
kodyId: input.codemod.kodyId,
specifier: input.codemod.specifier,
exportName: input.codemod.exportName ?? 'codemod',
}
: null,
revertOfRunId: input.revertOfRunId ?? null,
status: 'running',
createdAt: now,
updatedAt: now,
cursor: null,
items: [],
}
await saveRun(record)
return record
}
export async function resumeRun(input: {
runId: string
mode: RunMode
codemod: CodemodRef | null
revertOfRunId?: string | null
}): Promise<RunRecord> {
const existing = await getRun(input.runId)
if (!existing) {
throw new Error(`Run "${input.runId}" was not found.`)
}
if (existing.mode !== input.mode) {
throw new Error(
`Run "${input.runId}" is mode "${existing.mode}", not "${input.mode}".`,
)
}
if (input.codemod) {
const exportName = input.codemod.exportName ?? 'codemod'
if (
existing.codemod?.kodyId !== input.codemod.kodyId ||
existing.codemod?.specifier !== input.codemod.specifier ||
existing.codemod?.exportName !== exportName
) {
throw new Error(
`Run "${input.runId}" was created for a different codemod.`,
)
}
}
if (
input.revertOfRunId != null &&
existing.revertOfRunId !== input.revertOfRunId
) {
throw new Error(
`Run "${input.runId}" reverts a different run than requested.`,
)
}
return existing
}
export async function saveSnapshot(input: {
runId: string
packageId: string
beforeCommit: string
files: Record<string, string>
}): Promise<string> {
const key = `snapshot:${input.runId}:${input.packageId}`
await setWithRetry(key, {
beforeCommit: input.beforeCommit,
files: input.files,
})
return key
}
export async function getSnapshot(input: {
runId: string
packageId: string
}): Promise<{ beforeCommit: string; files: Record<string, string> } | null> {
const snapshot = (await storage().get(
`snapshot:${input.runId}:${input.packageId}`,
)) as { beforeCommit: string; files: Record<string, string> } | null
return snapshot ?? null
}
export async function markSourceItemReverted(input: {
runId: string
packageId: string
}): Promise<void> {
const run = await getRun(input.runId)
if (!run) return
const item = run.items.find((entry) => entry.packageId === input.packageId)
if (!item || item.status !== 'applied') return
item.status = 'reverted'
await saveRun(run)
}
export function summarizeItems(
items: Array<RunItem>,
): Partial<Record<ItemStatus, number>> {
const summary: Partial<Record<ItemStatus, number>> = {}
for (const item of items) {
summary[item.status] = (summary[item.status] ?? 0) + 1
}
return summary
}