diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index ed1b155003d..1e720b5a523 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -1014,7 +1014,7 @@ export namespace Config { }) .optional(), plugin: z.string().array().optional(), - snapshot: z.boolean().optional(), + snapshot: z.union([z.boolean(), z.number().int().nonnegative()]).optional(), share: z .enum(["manual", "auto", "disabled"]) .optional() diff --git a/packages/opencode/src/snapshot/index.ts b/packages/opencode/src/snapshot/index.ts index b3c8a905c25..0ea5bd8235f 100644 --- a/packages/opencode/src/snapshot/index.ts +++ b/packages/opencode/src/snapshot/index.ts @@ -11,7 +11,6 @@ import { Scheduler } from "../scheduler" export namespace Snapshot { const log = Log.create({ service: "snapshot" }) const hour = 60 * 60 * 1000 - const prune = "7.days" export function init() { Scheduler.register({ @@ -23,34 +22,39 @@ export namespace Snapshot { } export async function cleanup() { - if (Instance.project.vcs !== "git") return const cfg = await Config.get() - if (cfg.snapshot === false) return - const git = gitdir() - const exists = await fs - .stat(git) - .then(() => true) - .catch(() => false) - if (!exists) return - const result = await $`git --git-dir ${git} --work-tree ${Instance.worktree} gc --prune=${prune}` - .quiet() - .cwd(Instance.directory) - .nothrow() - if (result.exitCode !== 0) { - log.warn("cleanup failed", { - exitCode: result.exitCode, - stderr: result.stderr.toString(), - stdout: result.stdout.toString(), - }) - return + if (cfg.snapshot === false || cfg.snapshot === 0 || cfg.snapshot === undefined) return + const retentionDays: number = cfg.snapshot === true ? 7 : cfg.snapshot! + const snapshotDir = gitdir() + const parentDir = path.dirname(snapshotDir) + try { + const entries = await fs.readdir(parentDir, { withFileTypes: true }) + let deletedCount = 0 + for (const entry of entries) { + if (!entry.isDirectory()) continue + const projectDir = path.join(parentDir, entry.name) + const stats = await fs.stat(projectDir) + const ageMs = Date.now() - stats.mtimeMs + const ageDays = ageMs / (24 * 60 * 60 * 1000) + if (ageDays > retentionDays) { + await fs.rm(projectDir, { recursive: true, force: true }) + deletedCount++ + log.info("deleted old snapshot directory", { + project: entry.name, + ageDays: Math.floor(ageDays), + }) + } + } + log.info("cleanup", { retentionDays, deletedCount }) + } catch (error) { + log.warn("cleanup failed", { error: (error as Error).message }) } - log.info("cleanup", { prune }) } export async function track() { if (Instance.project.vcs !== "git") return const cfg = await Config.get() - if (cfg.snapshot === false) return + if (cfg.snapshot === false || cfg.snapshot === 0) return const git = gitdir() if (await fs.mkdir(git, { recursive: true })) { await $`git init` diff --git a/packages/opencode/test/snapshot/snapshot.test.ts b/packages/opencode/test/snapshot/snapshot.test.ts index 091469ec761..c090ff1b49a 100644 --- a/packages/opencode/test/snapshot/snapshot.test.ts +++ b/packages/opencode/test/snapshot/snapshot.test.ts @@ -2,6 +2,7 @@ import { test, expect } from "bun:test" import { $ } from "bun" import { Snapshot } from "../../src/snapshot" import { Instance } from "../../src/project/instance" +import { Config } from "../../src/config/config" import { tmpdir } from "../fixture/fixture" async function bootstrap() { @@ -1038,3 +1039,16 @@ test("diffFull with whitespace changes", async () => { }, }) }) + +test("snapshot config with boolean true uses default 7-day retention", async () => { + const cfg = { snapshot: true as true | number } + const retentionDays = cfg.snapshot === true ? 7 : cfg.snapshot + expect(retentionDays).toBe(7) +}) + +test("snapshot config with positive integer uses specified retention", async () => { + const cfg = { snapshot: 3 as true | number } + const retentionDays = cfg.snapshot === true ? 7 : cfg.snapshot + expect(retentionDays).toBe(3) +}) + diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index cb1606e3f61..14688851eab 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1667,7 +1667,7 @@ export type Config = { ignore?: Array } plugin?: Array - snapshot?: boolean + snapshot?: boolean | number /** * Control sharing behavior:'manual' allows manual sharing via commands, 'auto' enables automatic sharing, 'disabled' disables all sharing */