mirror of
https://github.com/glittercowboy/get-shit-done
synced 2026-04-25 17:25:23 +02:00
Merge pull request #1264 from gsd-build/fix/tmp-file-cleanup-1251
fix: temp file reaper prevents unbounded /tmp growth
This commit is contained in:
@@ -112,6 +112,40 @@ function findProjectRoot(startDir) {
|
||||
|
||||
// ─── Output helpers ───────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Remove stale gsd-* temp files/dirs older than maxAgeMs (default: 5 minutes).
|
||||
* Runs opportunistically before each new temp file write to prevent unbounded accumulation.
|
||||
* @param {string} prefix - filename prefix to match (e.g., 'gsd-')
|
||||
* @param {object} opts
|
||||
* @param {number} opts.maxAgeMs - max age in ms before removal (default: 5 min)
|
||||
* @param {boolean} opts.dirsOnly - if true, only remove directories (default: false)
|
||||
*/
|
||||
function reapStaleTempFiles(prefix = 'gsd-', { maxAgeMs = 5 * 60 * 1000, dirsOnly = false } = {}) {
|
||||
try {
|
||||
const tmpDir = require('os').tmpdir();
|
||||
const now = Date.now();
|
||||
const entries = fs.readdirSync(tmpDir);
|
||||
for (const entry of entries) {
|
||||
if (!entry.startsWith(prefix)) continue;
|
||||
const fullPath = path.join(tmpDir, entry);
|
||||
try {
|
||||
const stat = fs.statSync(fullPath);
|
||||
if (now - stat.mtimeMs > maxAgeMs) {
|
||||
if (stat.isDirectory()) {
|
||||
fs.rmSync(fullPath, { recursive: true, force: true });
|
||||
} else if (!dirsOnly) {
|
||||
fs.unlinkSync(fullPath);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// File may have been removed between readdir and stat — ignore
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Non-critical — don't let cleanup failures break output
|
||||
}
|
||||
}
|
||||
|
||||
function output(result, raw, rawValue) {
|
||||
if (raw && rawValue !== undefined) {
|
||||
process.stdout.write(String(rawValue));
|
||||
@@ -120,6 +154,7 @@ function output(result, raw, rawValue) {
|
||||
// Large payloads exceed Claude Code's Bash tool buffer (~50KB).
|
||||
// Write to tmpfile and output the path prefixed with @file: so callers can detect it.
|
||||
if (json.length > 50000) {
|
||||
reapStaleTempFiles();
|
||||
const tmpPath = path.join(require('os').tmpdir(), `gsd-${Date.now()}.json`);
|
||||
fs.writeFileSync(tmpPath, json, 'utf-8');
|
||||
process.stdout.write('@file:' + tmpPath);
|
||||
@@ -1005,6 +1040,7 @@ module.exports = {
|
||||
withPlanningLock,
|
||||
findProjectRoot,
|
||||
detectSubRepos,
|
||||
reapStaleTempFiles,
|
||||
MODEL_ALIAS_MAP,
|
||||
planningDir,
|
||||
planningPaths,
|
||||
|
||||
@@ -12,7 +12,7 @@ const fs = require('fs');
|
||||
const path = require('path');
|
||||
const os = require('os');
|
||||
const readline = require('readline');
|
||||
const { output, error, safeReadFile } = require('./core.cjs');
|
||||
const { output, error, safeReadFile, reapStaleTempFiles } = require('./core.cjs');
|
||||
|
||||
// ─── Session I/O Helpers ──────────────────────────────────────────────────────
|
||||
|
||||
@@ -333,6 +333,7 @@ async function cmdExtractMessages(projectArg, options, raw, overridePath) {
|
||||
sessions = sessions.slice(0, options.limit);
|
||||
}
|
||||
|
||||
reapStaleTempFiles('gsd-pipeline-', { dirsOnly: true });
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-pipeline-'));
|
||||
const outputPath = path.join(tmpDir, 'extracted-messages.jsonl');
|
||||
|
||||
@@ -511,6 +512,7 @@ async function cmdProfileSample(overridePath, options, raw) {
|
||||
}
|
||||
}
|
||||
|
||||
reapStaleTempFiles('gsd-profile-', { dirsOnly: true });
|
||||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-profile-'));
|
||||
const outputPath = path.join(tmpDir, 'profile-sample.jsonl');
|
||||
for (const msg of allMessages) {
|
||||
|
||||
@@ -18,6 +18,7 @@ const {
|
||||
escapeRegex,
|
||||
generateSlugInternal,
|
||||
normalizePhaseName,
|
||||
reapStaleTempFiles,
|
||||
normalizeMd,
|
||||
comparePhaseNum,
|
||||
safeReadFile,
|
||||
@@ -1315,3 +1316,51 @@ describe('findProjectRoot', () => {
|
||||
assert.strictEqual(findProjectRoot(backendDir), backendDir);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── reapStaleTempFiles ─────────────────────────────────────────────────────
|
||||
|
||||
describe('reapStaleTempFiles', () => {
|
||||
test('removes stale gsd-*.json files older than maxAgeMs', () => {
|
||||
const tmpDir = os.tmpdir();
|
||||
const stalePath = path.join(tmpDir, `gsd-reap-test-${Date.now()}.json`);
|
||||
fs.writeFileSync(stalePath, '{}');
|
||||
// Set mtime to 10 minutes ago
|
||||
const oldTime = new Date(Date.now() - 10 * 60 * 1000);
|
||||
fs.utimesSync(stalePath, oldTime, oldTime);
|
||||
|
||||
reapStaleTempFiles('gsd-reap-test-', { maxAgeMs: 5 * 60 * 1000 });
|
||||
|
||||
assert.ok(!fs.existsSync(stalePath), 'stale file should be removed');
|
||||
});
|
||||
|
||||
test('preserves fresh gsd-*.json files', () => {
|
||||
const tmpDir = os.tmpdir();
|
||||
const freshPath = path.join(tmpDir, `gsd-reap-fresh-${Date.now()}.json`);
|
||||
fs.writeFileSync(freshPath, '{}');
|
||||
|
||||
reapStaleTempFiles('gsd-reap-fresh-', { maxAgeMs: 5 * 60 * 1000 });
|
||||
|
||||
assert.ok(fs.existsSync(freshPath), 'fresh file should be preserved');
|
||||
// Clean up
|
||||
fs.unlinkSync(freshPath);
|
||||
});
|
||||
|
||||
test('removes stale temp directories when present', () => {
|
||||
const tmpDir = os.tmpdir();
|
||||
const staleDir = fs.mkdtempSync(path.join(tmpDir, 'gsd-reap-dir-'));
|
||||
fs.writeFileSync(path.join(staleDir, 'data.jsonl'), 'test');
|
||||
// Set mtime to 10 minutes ago
|
||||
const oldTime = new Date(Date.now() - 10 * 60 * 1000);
|
||||
fs.utimesSync(staleDir, oldTime, oldTime);
|
||||
|
||||
reapStaleTempFiles('gsd-reap-dir-', { maxAgeMs: 5 * 60 * 1000 });
|
||||
|
||||
assert.ok(!fs.existsSync(staleDir), 'stale directory should be removed');
|
||||
});
|
||||
|
||||
test('does not throw on empty or missing prefix matches', () => {
|
||||
assert.doesNotThrow(() => {
|
||||
reapStaleTempFiles('gsd-nonexistent-prefix-xyz-', { maxAgeMs: 0 });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user