wtagent 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,125 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { replaceFileAtomic } from "../shared/atomic-write.js";
4
+
5
+ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
6
+
7
+ export function localDateString(now = new Date()) {
8
+ const year = now.getFullYear();
9
+ const month = String(now.getMonth() + 1).padStart(2, "0");
10
+ const day = String(now.getDate()).padStart(2, "0");
11
+ return `${year}-${month}-${day}`;
12
+ }
13
+
14
+ function isDateString(value) {
15
+ return typeof value === "string" && DATE_RE.test(value);
16
+ }
17
+
18
+ function normalizeItems(items) {
19
+ const normalized = {};
20
+ if (!items || typeof items !== "object" || Array.isArray(items)) {
21
+ return normalized;
22
+ }
23
+ for (const [id, item] of Object.entries(items)) {
24
+ if (!id || !item || typeof item !== "object") {
25
+ continue;
26
+ }
27
+ const shown = Number.isFinite(item.shown) ? Math.max(0, Math.floor(item.shown)) : 0;
28
+ normalized[id] = {
29
+ shown,
30
+ lastShownDate: isDateString(item.lastShownDate) ? item.lastShownDate : null,
31
+ };
32
+ }
33
+ return normalized;
34
+ }
35
+
36
+ function normalizeData(parsed) {
37
+ return {
38
+ update: {
39
+ lastPromptDate: isDateString(parsed?.update?.lastPromptDate)
40
+ ? parsed.update.lastPromptDate
41
+ : null,
42
+ },
43
+ notices: {
44
+ lastShownDate: isDateString(parsed?.notices?.lastShownDate)
45
+ ? parsed.notices.lastShownDate
46
+ : null,
47
+ items: normalizeItems(parsed?.notices?.items),
48
+ },
49
+ };
50
+ }
51
+
52
+ export function getNoticeStorePath(appDataDir) {
53
+ return path.join(appDataDir, "notices.json");
54
+ }
55
+
56
+ // Persists the once-a-day update prompt and per-id notice impressions.
57
+ export class NoticeStore {
58
+ constructor({ filePath, now = () => new Date() }) {
59
+ this.filePath = filePath;
60
+ this.now = now;
61
+ this.data = normalizeData(null);
62
+ this.loaded = false;
63
+ }
64
+
65
+ today() {
66
+ return localDateString(this.now());
67
+ }
68
+
69
+ async ensureLoaded() {
70
+ if (this.loaded) {
71
+ return;
72
+ }
73
+ this.loaded = true;
74
+ try {
75
+ this.data = normalizeData(JSON.parse(await fs.readFile(this.filePath, "utf8")));
76
+ } catch (error) {
77
+ if (error.code !== "ENOENT") {
78
+ this.data = normalizeData(null);
79
+ }
80
+ }
81
+ }
82
+
83
+ wasUpdatePromptedToday() {
84
+ return this.data.update.lastPromptDate === this.today();
85
+ }
86
+
87
+ wasNoticeShownToday() {
88
+ return this.data.notices.lastShownDate === this.today();
89
+ }
90
+
91
+ shownCount(id) {
92
+ return this.data.notices.items[id]?.shown ?? 0;
93
+ }
94
+
95
+ async markUpdatePrompted() {
96
+ this.data.update.lastPromptDate = this.today();
97
+ await this.save();
98
+ }
99
+
100
+ async recordNoticeShown(id) {
101
+ const today = this.today();
102
+ const previous = this.data.notices.items[id] ?? { shown: 0, lastShownDate: null };
103
+ this.data.notices.lastShownDate = today;
104
+ this.data.notices.items[id] = {
105
+ shown: previous.shown + 1,
106
+ lastShownDate: today,
107
+ };
108
+ await this.save();
109
+ }
110
+
111
+ async save() {
112
+ await fs.mkdir(path.dirname(this.filePath), { recursive: true });
113
+ const temporary = `${this.filePath}.${process.pid}.tmp`;
114
+ await fs.writeFile(
115
+ temporary,
116
+ `${JSON.stringify(this.data, null, 2)}\n`,
117
+ { mode: 0o600 },
118
+ );
119
+ try {
120
+ await replaceFileAtomic(temporary, this.filePath);
121
+ } finally {
122
+ await fs.rm(temporary, { force: true }).catch(() => {});
123
+ }
124
+ }
125
+ }