paqad-ai
Version:
Spec-driven development framework — AI agents that think before they type
137 lines (134 loc) • 5.28 kB
JavaScript
import { createRequire as __paqadCreateRequire } from 'module'; const require = __paqadCreateRequire(import.meta.url);
import {
renameWithRetrySync
} from "./chunk-Z3NGWHFX.js";
import {
init_esm_shims
} from "./chunk-IDFRWBMN.js";
// src/update/last-update.ts
init_esm_shims();
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "fs";
import { dirname, join } from "path";
var LAST_UPDATE_FILE = ".paqad/state/last-update.json";
var LAST_UPDATE_SEEN_FILE = ".paqad/state/last-update.seen.json";
var VERSION_PATTERN = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.+-]+)?$/u;
var PERSONAL_FILE = /(^|\/)settings\.local\.json$/u;
var SESSION_LINE_TAIL = "Tell the developer, and run paqad-ai update --last to see the details.";
function allChangedFiles(report) {
return [.../* @__PURE__ */ new Set([...report.regenerated, ...report.changed_files ?? []])].filter(
(path) => !PERSONAL_FILE.test(path)
);
}
function isStringList(value) {
return Array.isArray(value) && value.every((item) => typeof item === "string");
}
function isVersion(value) {
return typeof value === "string" && VERSION_PATTERN.test(value);
}
function parseRecord(text) {
let parsed;
try {
parsed = JSON.parse(text);
} catch {
return null;
}
if (parsed === null || typeof parsed !== "object") return null;
const record = parsed;
const valid = (record.previous_version === null || isVersion(record.previous_version)) && isVersion(record.target_version) && isStringList(record.changed_files) && isStringList(record.skipped) && isStringList(record.config_keys_pruned) && isStringList(record.team_setup_notes) && isStringList(record.git_workflow_notes) && typeof record.updated_at === "string";
return valid ? record : null;
}
function readRecordFile(path) {
let text;
try {
text = readFileSync(path, "utf8");
} catch {
return null;
}
return parseRecord(text);
}
function readLastUpdateRecord(projectRoot) {
return readRecordFile(join(projectRoot, LAST_UPDATE_FILE)) ?? readRecordFile(join(projectRoot, LAST_UPDATE_SEEN_FILE));
}
function hasSomethingToReport(record) {
return record.changed_files.length > 0 || record.config_keys_pruned.length > 0 || record.team_setup_notes.length > 0 || record.git_workflow_notes.length > 0;
}
function union(first, second) {
return [.../* @__PURE__ */ new Set([...first, ...second])];
}
function mergeRecords(older, newer) {
return {
previous_version: older.previous_version,
target_version: newer.target_version,
changed_files: union(older.changed_files, newer.changed_files),
skipped: union(older.skipped, newer.skipped),
config_keys_pruned: union(older.config_keys_pruned, newer.config_keys_pruned),
team_setup_notes: union(older.team_setup_notes, newer.team_setup_notes),
git_workflow_notes: union(older.git_workflow_notes, newer.git_workflow_notes),
updated_at: newer.updated_at
};
}
function removeQuietly(path) {
try {
rmSync(path, { force: true });
} catch {
return;
}
}
function writeLastUpdateRecord(projectRoot, report, updatedAt = (/* @__PURE__ */ new Date()).toISOString(), io = {}) {
const record = {
previous_version: report.previous_version,
target_version: report.target_version,
changed_files: allChangedFiles(report),
skipped: report.skipped.map((entry) => entry.path),
config_keys_pruned: report.config_keys_pruned,
team_setup_notes: report.team_setup_notes ?? [],
git_workflow_notes: report.git_workflow_notes ?? [],
updated_at: updatedAt
};
if (!hasSomethingToReport(record)) return false;
const target = join(projectRoot, LAST_UPDATE_FILE);
const temp = `${target}.${process.pid}.tmp`;
try {
const unseen = readRecordFile(target);
const merged = unseen === null ? record : mergeRecords(unseen, record);
mkdirSync(dirname(target), { recursive: true });
writeFileSync(temp, `${JSON.stringify(merged, null, 2)}
`, "utf8");
renameWithRetrySync(temp, target, io.rename ? { rename: io.rename } : {});
return true;
} catch {
removeQuietly(temp);
return false;
}
}
function lastUpdateSessionLine(record) {
const count = record.changed_files.length;
const outcome = count === 0 ? "has notes for you" : `changed ${count} ${count === 1 ? "file" : "files"}`;
const { previous_version: previous, target_version: target } = record;
if (previous === target) {
return `paqad refreshed its files on ${target} and ${outcome}. ${SESSION_LINE_TAIL}`;
}
return previous === null ? `paqad updated itself to ${target} and ${outcome}. ${SESSION_LINE_TAIL}` : `paqad updated itself from ${previous} to ${target} and ${outcome}. ${SESSION_LINE_TAIL}`;
}
function takeLastUpdateSessionLine(projectRoot, source, io = {}) {
if (source === "compact") return null;
const unseen = join(projectRoot, LAST_UPDATE_FILE);
const record = readRecordFile(unseen);
if (record === null) return null;
try {
renameWithRetrySync(unseen, join(projectRoot, LAST_UPDATE_SEEN_FILE), {
remove: () => void 0,
...io.rename ? { rename: io.rename } : {}
});
} catch {
return null;
}
return lastUpdateSessionLine(record);
}
export {
allChangedFiles,
readLastUpdateRecord,
writeLastUpdateRecord,
lastUpdateSessionLine,
takeLastUpdateSessionLine
};