@vanillagreen/pi-qol 2.2.1 → 2.3.4
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 +13 -0
- package/extensions/qol/compaction-handoff.ts +6 -19
- package/extensions/qol/compaction.ts +1 -1
- package/extensions/qol/constants.ts +12 -1
- package/extensions/qol/glyphs.ts +1 -1
- package/extensions/qol/images.ts +41 -6
- package/extensions/qol/notifications.ts +14 -1
- package/extensions/qol/package-config.ts +445 -0
- package/extensions/qol/session-search/cache.ts +175 -46
- package/extensions/qol/session-search/component.ts +104 -14
- package/extensions/qol/session-search/index.ts +28 -18
- package/extensions/qol/session-search/jsonl.ts +33 -1
- package/extensions/qol/session-search/regex-worker.mjs +12 -0
- package/extensions/qol/session-search/regex.ts +49 -0
- package/extensions/qol/session-search/search.ts +60 -166
- package/extensions/qol/session-search/types.ts +10 -0
- package/extensions/qol/settings.ts +5 -100
- package/extensions/qol.ts +50 -12
- package/package.json +4 -3
- package/scripts/lane-retention.ts +187 -0
package/extensions/qol.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { pruneLanes } from "../scripts/lane-retention.js";
|
|
2
4
|
import { truncateToWidth, type TUI } from "@earendil-works/pi-tui";
|
|
3
5
|
import { criticalInfo, lastAssistantTextFromAgentEnd, needsDirection, taskStats } from "./qol/agent-end.js";
|
|
4
6
|
import { getQuestionService, readCavemanBridge, type QuestionOpenedEventLike } from "./qol/bridges.js";
|
|
@@ -16,6 +18,7 @@ import {
|
|
|
16
18
|
DEFAULT_IDLE_COMPACTION_SECONDS,
|
|
17
19
|
DEFAULT_INPUT_BOTTOM_PADDING_LINES,
|
|
18
20
|
INSTALL_SYMBOL,
|
|
21
|
+
QOL_BUDGET_HANDOFF_FOLDER,
|
|
19
22
|
QOL_NOTIFICATION_SERVICE_SYMBOL,
|
|
20
23
|
QUESTION_OPENED_EVENT,
|
|
21
24
|
SESSION_MANAGER_STATUS_KEY,
|
|
@@ -23,6 +26,7 @@ import {
|
|
|
23
26
|
SESSION_SEARCH_STATUS_KEY,
|
|
24
27
|
SESSION_TITLE_SYNC_INTERVAL_MS,
|
|
25
28
|
STATUS_KEY,
|
|
29
|
+
THINKING_TIMER_MAX_DURATIONS,
|
|
26
30
|
THINKING_TIMER_STORE_SYMBOL,
|
|
27
31
|
TMUX_SESSION_TITLE_BORDER_FORMAT,
|
|
28
32
|
} from "./qol/constants.js";
|
|
@@ -35,10 +39,11 @@ import {
|
|
|
35
39
|
QolEditor,
|
|
36
40
|
} from "./qol/editor.js";
|
|
37
41
|
import { runHandoff } from "./qol/handoff.js";
|
|
38
|
-
import {
|
|
42
|
+
import { imageContentsForPaths, resolveSubmittedImagePaths } from "./qol/images.js";
|
|
39
43
|
import {
|
|
40
44
|
clearTmuxWindowMark,
|
|
41
45
|
notifyQuestionOpened,
|
|
46
|
+
resetQolNotificationCooldowns,
|
|
42
47
|
sendQolNotification,
|
|
43
48
|
type QolNotificationService,
|
|
44
49
|
} from "./qol/notifications.js";
|
|
@@ -61,15 +66,18 @@ import {
|
|
|
61
66
|
} from "./qol/session-rename.js";
|
|
62
67
|
import { createScheduleController, getScheduleArgumentCompletions } from "./qol/schedule.js";
|
|
63
68
|
import {
|
|
69
|
+
clearSessionSearchPendingAction,
|
|
64
70
|
consumePendingSessionSearchContext,
|
|
65
71
|
openQolSessionSearch,
|
|
66
|
-
qolSessionSearchPendingActions,
|
|
67
72
|
refreshQolSessionSearchCache,
|
|
73
|
+
releaseQolSessionSearchCache,
|
|
68
74
|
renderSessionSearchContextMessage,
|
|
69
75
|
runSessionSearchResumeOrFork,
|
|
70
76
|
sessionSearchShortcut,
|
|
77
|
+
takeSessionSearchPendingAction,
|
|
71
78
|
} from "./qol/session-search/index.js";
|
|
72
|
-
import {
|
|
79
|
+
import { installSettingsCacheRefresh, piUserDir, recordProjectTrust } from "./qol/package-config.js";
|
|
80
|
+
import { settingBoolean, settingNumber, settingString } from "./qol/settings.js";
|
|
73
81
|
import { statusMessage } from "./qol/status-message.js";
|
|
74
82
|
import {
|
|
75
83
|
formatTmuxSessionTitle,
|
|
@@ -248,11 +256,24 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
248
256
|
const duration = Math.max(0, endTimeMs - start);
|
|
249
257
|
thinkingTimerStore.starts.delete(key);
|
|
250
258
|
thinkingTimerStore.durations.set(key, duration);
|
|
259
|
+
for (const oldest of thinkingTimerStore.durations.keys()) {
|
|
260
|
+
if (thinkingTimerStore.durations.size <= THINKING_TIMER_MAX_DURATIONS) break;
|
|
261
|
+
thinkingTimerStore.durations.delete(oldest);
|
|
262
|
+
}
|
|
251
263
|
const label = thinkingTimerStore.labels.get(key);
|
|
252
264
|
if (label) label.setText(thinkingTimerLabel(thinkingTimerStore.theme, duration, thinkingTimerStore.cwd));
|
|
253
265
|
if (thinkingTimerStore.starts.size === 0) stopThinkingTimerTicker();
|
|
254
266
|
};
|
|
255
267
|
|
|
268
|
+
// A label is held only to tick it while its block runs. A finished block's
|
|
269
|
+
// label already shows its final time, and a redraw of that message labels
|
|
270
|
+
// it again from `durations`.
|
|
271
|
+
const releaseFinishedThinkingLabels = () => {
|
|
272
|
+
for (const key of thinkingTimerStore.labels.keys()) {
|
|
273
|
+
if (!thinkingTimerStore.starts.has(key)) thinkingTimerStore.labels.delete(key);
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
256
277
|
const clearIdleCompactionTimer = () => {
|
|
257
278
|
if (idleCompactionTimer) clearTimeout(idleCompactionTimer);
|
|
258
279
|
idleCompactionTimer = undefined;
|
|
@@ -586,8 +607,13 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
586
607
|
rateLimitAutoResumeController.noteExternalRateLimitEvent(payload, currentCtx);
|
|
587
608
|
});
|
|
588
609
|
|
|
610
|
+
installSettingsCacheRefresh(pi);
|
|
589
611
|
pi.on("session_start", (event, ctx) => {
|
|
590
612
|
recordProjectTrust(ctx);
|
|
613
|
+
const pruned = pruneLanes(join(piUserDir(), "kendex", "sessions"), QOL_BUDGET_HANDOFF_FOLDER.split("/"));
|
|
614
|
+
for (const failure of pruned.failed) {
|
|
615
|
+
console.warn(`handoff-prune-error: ${failure.path}\n${failure.error}`);
|
|
616
|
+
}
|
|
591
617
|
currentCtx = ctx;
|
|
592
618
|
subscribeCavemanBridge();
|
|
593
619
|
latestSystemPromptOptions = undefined;
|
|
@@ -648,7 +674,8 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
648
674
|
}
|
|
649
675
|
startQuestionSubscription(ctx);
|
|
650
676
|
void attemptAutoRename(ctx);
|
|
651
|
-
|
|
677
|
+
// A headless session has no search overlay to open, so it loads no index.
|
|
678
|
+
if (ctx.hasUI && settingBoolean("sessionSearch.enabled", true, ctx.cwd)) {
|
|
652
679
|
if (sessionSearchWarmupTimer) clearTimeout(sessionSearchWarmupTimer);
|
|
653
680
|
sessionSearchWarmupTimer = setTimeout(() => {
|
|
654
681
|
sessionSearchWarmupTimer = undefined;
|
|
@@ -673,8 +700,11 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
673
700
|
clearQuestionSubscribeTimer();
|
|
674
701
|
if (sessionSearchWarmupTimer) clearTimeout(sessionSearchWarmupTimer);
|
|
675
702
|
sessionSearchWarmupTimer = undefined;
|
|
703
|
+
releaseQolSessionSearchCache();
|
|
704
|
+
clearSessionSearchPendingAction();
|
|
676
705
|
resetThinkingTimer(undefined);
|
|
677
706
|
clearTmuxWindowMark(pi);
|
|
707
|
+
resetQolNotificationCooldowns();
|
|
678
708
|
questionUnsubscribe?.();
|
|
679
709
|
questionUnsubscribe = undefined;
|
|
680
710
|
currentCtx = undefined;
|
|
@@ -744,6 +774,7 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
744
774
|
}
|
|
745
775
|
});
|
|
746
776
|
pi.on("agent_end", (event, ctx) => {
|
|
777
|
+
releaseFinishedThinkingLabels();
|
|
747
778
|
if (ctx.hasUI) {
|
|
748
779
|
void refreshStatusline(ctx);
|
|
749
780
|
requestRender();
|
|
@@ -816,15 +847,24 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
816
847
|
if (event?.toolName === "tasks_write") maybeNotifyTaskCompletion(ctx, event.details?.state);
|
|
817
848
|
});
|
|
818
849
|
|
|
819
|
-
pi.on("input", async (event) => {
|
|
850
|
+
pi.on("input", async (event, ctx) => {
|
|
820
851
|
clearTmuxWindowMark(pi);
|
|
821
852
|
if (event.source === "extension") return { action: "continue" };
|
|
822
853
|
const text = event.text ?? "";
|
|
823
|
-
const paths =
|
|
854
|
+
const paths = resolveSubmittedImagePaths(text, ctx.cwd);
|
|
824
855
|
if (paths.length === 0) return { action: "continue" };
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
856
|
+
try {
|
|
857
|
+
const images = await imageContentsForPaths(paths, event.images ?? []);
|
|
858
|
+
return { action: "transform", images: [...(event.images ?? []), ...images], text: event.text };
|
|
859
|
+
} catch (error) {
|
|
860
|
+
const message = `Image submission refused: ${stringifyError(error)}`;
|
|
861
|
+
ctx.ui.notify(message, "error");
|
|
862
|
+
// Print and JSON modes reserve stderr for diagnostics; input exceptions
|
|
863
|
+
// would let Pi continue submission instead of refusing the image.
|
|
864
|
+
if (!ctx.hasUI) console.error(message);
|
|
865
|
+
if (ctx.hasUI && ctx.ui.getEditorText() === "") ctx.ui.setEditorText(text);
|
|
866
|
+
return { action: "handled" };
|
|
867
|
+
}
|
|
828
868
|
});
|
|
829
869
|
|
|
830
870
|
if (settingBoolean("enableSessionNameCommand", true)) {
|
|
@@ -903,13 +943,11 @@ export default function qol(pi: ExtensionAPI): void {
|
|
|
903
943
|
pi.registerCommand("search:resume-pending", {
|
|
904
944
|
description: "Run a pending session-search resume or fork action",
|
|
905
945
|
handler: async (args, ctx) => {
|
|
906
|
-
const
|
|
907
|
-
const action = qolSessionSearchPendingActions.get(id);
|
|
946
|
+
const action = takeSessionSearchPendingAction(args.trim());
|
|
908
947
|
if (!action) {
|
|
909
948
|
ctx.ui.notify("No pending session-search resume/fork action found.", "warning");
|
|
910
949
|
return;
|
|
911
950
|
}
|
|
912
|
-
qolSessionSearchPendingActions.delete(id);
|
|
913
951
|
if (!(await runSessionSearchResumeOrFork(pi, ctx, action))) ctx.ui.notify("Session resume/fork is unavailable in this context.", "error");
|
|
914
952
|
},
|
|
915
953
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vanillagreen/pi-qol",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.4",
|
|
4
4
|
"description": "Pi quality-of-life extension: compact statusline/π prompt, reliable multiline input, styled pasted-image chips, session naming/search/context import, scheduled prompts, handoff, permission prompts, notifications, custom compaction, and a collapsed-thinking timer.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -504,9 +504,9 @@
|
|
|
504
504
|
{
|
|
505
505
|
"key": "sessionSearch.cacheTtlSeconds",
|
|
506
506
|
"label": "Session cache TTL",
|
|
507
|
-
"description": "Seconds
|
|
507
|
+
"description": "Seconds a loaded session index is kept in memory before it is released; the next search opens at once and reloads it. 0 keeps the index until the session ends.",
|
|
508
508
|
"type": "number",
|
|
509
|
-
"default":
|
|
509
|
+
"default": 300,
|
|
510
510
|
"category": "Session Search",
|
|
511
511
|
"apply": "live"
|
|
512
512
|
},
|
|
@@ -988,6 +988,7 @@
|
|
|
988
988
|
},
|
|
989
989
|
"files": [
|
|
990
990
|
"extensions/",
|
|
991
|
+
"scripts/",
|
|
991
992
|
"assets/",
|
|
992
993
|
"README.md",
|
|
993
994
|
"package.json"
|
|
@@ -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
|
+
}
|