@vanillagreen/pi-qol 2.2.0 → 2.3.2
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.
- package/README.md +11 -0
- package/extensions/qol/compaction-handoff.ts +6 -19
- package/extensions/qol/compaction.ts +1 -1
- package/extensions/qol/constants.ts +14 -1
- package/extensions/qol/glyphs.ts +1 -1
- package/extensions/qol/images.ts +2 -1
- package/extensions/qol/notifications.ts +172 -97
- package/extensions/qol/package-config.ts +445 -0
- package/extensions/qol/pending-queue.ts +49 -4
- package/extensions/qol/session-search/cache.ts +68 -17
- package/extensions/qol/session-search/component.ts +19 -5
- package/extensions/qol/session-search/index.ts +17 -9
- package/extensions/qol/session-search/types.ts +7 -0
- package/extensions/qol/settings.ts +5 -100
- package/extensions/qol.ts +50 -19
- package/package.json +4 -3
- package/scripts/lane-retention.ts +187 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lane file retention: the one rule for files a Pi extension package keeps on
|
|
3
|
+
* disk for a lane (fetched text, transcripts, task logs). A lane's files are
|
|
4
|
+
* deleted once the lane's working directory is gone, which is how a merged
|
|
5
|
+
* lane's worktree ends, and no file is kept past LANE_FILE_MAX_AGE_MS.
|
|
6
|
+
*
|
|
7
|
+
* A lane directory is one the package made with openLaneDir: a real directory,
|
|
8
|
+
* owned by this user, that holds a LANE_CWD_FILE record. The prune touches
|
|
9
|
+
* nothing else under its root, so a folder another tool keeps there, or a
|
|
10
|
+
* symbolic link to one, is left alone.
|
|
11
|
+
*
|
|
12
|
+
* Vendored byte-identical into every package that keeps such files, under
|
|
13
|
+
* `scripts/`; pi-extensions/package-policy.test.mjs holds the copies equal.
|
|
14
|
+
* Edit one copy, then copy it over the others.
|
|
15
|
+
*/
|
|
16
|
+
import { lstatSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
|
|
19
|
+
export const LANE_FILE_MAX_AGE_MS = 5 * 24 * 60 * 60 * 1000;
|
|
20
|
+
|
|
21
|
+
/** The file in a lane directory that names the lane's working directory. */
|
|
22
|
+
export const LANE_CWD_FILE = ".lane-cwd";
|
|
23
|
+
|
|
24
|
+
export interface LanePruneFailure {
|
|
25
|
+
path: string;
|
|
26
|
+
error: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface LanePruneResult {
|
|
30
|
+
removed: string[];
|
|
31
|
+
failed: LanePruneFailure[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Create `dir` as a lane directory for `cwd`. Rewriting the record marks the
|
|
35
|
+
* lane live, so its directory is not pruned as empty and old. */
|
|
36
|
+
export function openLaneDir(dir: string, cwd: string): string {
|
|
37
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
38
|
+
writeFileSync(join(dir, LANE_CWD_FILE), cwd, { mode: 0o600 });
|
|
39
|
+
return dir;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function message(error: unknown): string {
|
|
43
|
+
return error instanceof Error ? error.message : String(error);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Whether `path` is a real directory, not a symbolic link, owned by this
|
|
47
|
+
* user. A platform without user ids (Windows) checks the type alone. */
|
|
48
|
+
function ownDirectory(path: string): boolean {
|
|
49
|
+
let stat;
|
|
50
|
+
try {
|
|
51
|
+
stat = lstatSync(path);
|
|
52
|
+
} catch {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
const uid = process.getuid?.();
|
|
56
|
+
return stat.isDirectory() && (uid === undefined || stat.uid === uid);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function remove(path: string, result: LanePruneResult): void {
|
|
60
|
+
try {
|
|
61
|
+
rmSync(path, { recursive: true, force: true });
|
|
62
|
+
result.removed.push(path);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
result.failed.push({ path, error: message(error) });
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The lane's recorded working directory; undefined when `dir` holds no
|
|
69
|
+
* record, so the package did not make it. */
|
|
70
|
+
function recordedCwd(dir: string): string | undefined {
|
|
71
|
+
try {
|
|
72
|
+
const cwd = readFileSync(join(dir, LANE_CWD_FILE), "utf8");
|
|
73
|
+
return cwd === "" ? undefined : cwd;
|
|
74
|
+
} catch {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Whether a lane's recorded working directory is still there. Only a stat
|
|
80
|
+
* that answers ENOENT or ENOTDIR confirms it is gone. Any other failure, such
|
|
81
|
+
* as a refused permission or a filesystem that is briefly unavailable, is
|
|
82
|
+
* reported against the lane and answers "unknown", so the lane is not
|
|
83
|
+
* removed as gone. */
|
|
84
|
+
function cwdState(dir: string, cwd: string, result: LanePruneResult): "present" | "gone" | "unknown" {
|
|
85
|
+
try {
|
|
86
|
+
statSync(cwd);
|
|
87
|
+
return "present";
|
|
88
|
+
} catch (error) {
|
|
89
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
90
|
+
if (code === "ENOENT" || code === "ENOTDIR") return "gone";
|
|
91
|
+
result.failed.push({ path: dir, error: `lane-cwd-unchecked: the lane is kept; its working directory could not be checked: ${message(error)}` });
|
|
92
|
+
return "unknown";
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function modifiedAt(path: string, result: LanePruneResult): number | undefined {
|
|
97
|
+
try {
|
|
98
|
+
return lstatSync(path).mtimeMs;
|
|
99
|
+
} catch (error) {
|
|
100
|
+
result.failed.push({ path, error: message(error) });
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Remove the files under `dir` older than `cutoff`, never the lane record;
|
|
106
|
+
* return how many entries other than the record remain. */
|
|
107
|
+
function pruneOldFiles(dir: string, cutoff: number, result: LanePruneResult): number {
|
|
108
|
+
let remaining = 0;
|
|
109
|
+
let entries;
|
|
110
|
+
try {
|
|
111
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
112
|
+
} catch (error) {
|
|
113
|
+
result.failed.push({ path: dir, error: message(error) });
|
|
114
|
+
return 1;
|
|
115
|
+
}
|
|
116
|
+
for (const entry of entries) {
|
|
117
|
+
const path = join(dir, entry.name);
|
|
118
|
+
if (entry.name === LANE_CWD_FILE) continue;
|
|
119
|
+
if (entry.isDirectory()) {
|
|
120
|
+
if (pruneOldFiles(path, cutoff, result) === 0) remove(path, result);
|
|
121
|
+
else remaining++;
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const mtime = modifiedAt(path, result);
|
|
125
|
+
if (mtime !== undefined && mtime < cutoff) remove(path, result);
|
|
126
|
+
else remaining++;
|
|
127
|
+
}
|
|
128
|
+
return remaining;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Apply the retention rule to each lane directory at `root/<entry>/<...below>`,
|
|
133
|
+
* one entry per session. A lane whose recorded working directory is gone is
|
|
134
|
+
* removed whole; a working directory that cannot be checked is reported and
|
|
135
|
+
* does not count as gone. Otherwise each file older than LANE_FILE_MAX_AGE_MS is
|
|
136
|
+
* removed, and the lane goes too once only its record is left and the record
|
|
137
|
+
* is that old. The record is kept while the lane is: it is what marks the
|
|
138
|
+
* directory as the package's. An absent root has no lanes. Every failure,
|
|
139
|
+
* including an unreadable root, is reported, never thrown, so the caller's
|
|
140
|
+
* other session_start work still runs.
|
|
141
|
+
*/
|
|
142
|
+
export function pruneLanes(root: string, below: string[] = [], now = Date.now()): LanePruneResult {
|
|
143
|
+
const result: LanePruneResult = { removed: [], failed: [] };
|
|
144
|
+
let entries: string[];
|
|
145
|
+
try {
|
|
146
|
+
entries = readdirSync(root);
|
|
147
|
+
} catch (error) {
|
|
148
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") result.failed.push({ path: root, error: message(error) });
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
if (!ownDirectory(root)) {
|
|
152
|
+
result.failed.push({ path: root, error: "lane-root-not-owned: not a directory this user owns; nothing under it was pruned" });
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
const cutoff = now - LANE_FILE_MAX_AGE_MS;
|
|
156
|
+
for (const entry of entries) {
|
|
157
|
+
let dir = root;
|
|
158
|
+
let owned = true;
|
|
159
|
+
for (const part of [entry, ...below]) {
|
|
160
|
+
dir = join(dir, part);
|
|
161
|
+
if (!ownDirectory(dir)) {
|
|
162
|
+
owned = false;
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (!owned) continue;
|
|
167
|
+
const cwd = recordedCwd(dir);
|
|
168
|
+
if (cwd === undefined) continue;
|
|
169
|
+
const state = cwdState(dir, cwd, result);
|
|
170
|
+
switch (state) {
|
|
171
|
+
case "gone":
|
|
172
|
+
remove(dir, result);
|
|
173
|
+
continue;
|
|
174
|
+
case "present":
|
|
175
|
+
case "unknown":
|
|
176
|
+
break;
|
|
177
|
+
default: {
|
|
178
|
+
const unhandled: never = state;
|
|
179
|
+
throw new Error(`lane-cwd-state-unhandled: ${String(unhandled)}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (pruneOldFiles(dir, cutoff, result) > 0) continue;
|
|
183
|
+
const recordAt = modifiedAt(join(dir, LANE_CWD_FILE), result);
|
|
184
|
+
if (recordAt !== undefined && recordAt < cutoff) remove(dir, result);
|
|
185
|
+
}
|
|
186
|
+
return result;
|
|
187
|
+
}
|