github-search-cli 9.4.1 → 9.4.3
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/CHANGELOG.md +65 -1
- package/README.md +268 -237
- package/bin/ghfind.js +98 -89
- package/completions/ghfind.bash +1 -1
- package/completions/ghfind.fish +3 -1
- package/completions/ghfind.zsh +6 -0
- package/dist/{chunk-IVUU2KGR.js → chunk-GYIL4S5X.js} +4 -1
- package/dist/cli.js +1179 -403
- package/dist/{config-GNZOQRTH.js → config-OCIPF2IF.js} +1 -1
- package/package.json +16 -5
- package/release-notes/9.3.0.md +32 -29
- package/scripts/build.ts +34 -19
- package/scripts/generate-svg-demo.js +218 -168
- package/scripts/postinstall.js +176 -176
- package/src/bookmarks.ts +69 -69
- package/src/cli.ts +650 -566
- package/src/compare.ts +69 -47
- package/src/config.ts +70 -65
- package/src/deepdive.ts +149 -118
- package/src/doctor.ts +287 -287
- package/src/error-report.ts +8 -12
- package/src/errors.ts +62 -59
- package/src/explore.ts +34 -29
- package/src/export.ts +73 -63
- package/src/help.ts +157 -157
- package/src/history.ts +69 -55
- package/src/init.ts +60 -55
- package/src/landing.ts +106 -104
- package/src/login.ts +186 -0
- package/src/markdown-render.ts +1 -6
- package/src/notifications.ts +75 -59
- package/src/open-url.ts +16 -16
- package/src/org.ts +299 -0
- package/src/output.ts +153 -137
- package/src/package-browser.ts +317 -305
- package/src/package.ts +149 -137
- package/src/pipe.ts +60 -54
- package/src/releases.ts +182 -144
- package/src/saved-searches.ts +72 -50
- package/src/search-state.ts +283 -229
- package/src/search.ts +678 -653
- package/src/session.ts +26 -24
- package/src/share.ts +61 -54
- package/src/status.ts +68 -61
- package/src/storage.ts +136 -106
- package/src/themes.ts +259 -116
- package/src/tips.ts +42 -42
- package/src/toast.ts +70 -69
- package/src/trending-parser.ts +104 -0
- package/src/trending.ts +481 -462
- package/src/tui.ts +3015 -2867
- package/src/types.ts +160 -158
- package/src/update-check.ts +231 -215
- package/src/version.ts +33 -0
- package/src/watch.ts +95 -74
package/src/session.ts
CHANGED
|
@@ -1,24 +1,26 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Session restore — save/restore UI state between restarts.
|
|
3
|
-
*/
|
|
4
|
-
import type { SessionState } from "./types";
|
|
5
|
-
import { readJSON, writeJSON } from "./storage";
|
|
6
|
-
|
|
7
|
-
const SESSION_FILE = "session.json";
|
|
8
|
-
|
|
9
|
-
export function saveSession(state: SessionState): void {
|
|
10
|
-
try {
|
|
11
|
-
writeJSON(SESSION_FILE, state);
|
|
12
|
-
} catch (err) {
|
|
13
|
-
if (process.env.DEBUG)
|
|
14
|
-
console.error(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Session restore — save/restore UI state between restarts.
|
|
3
|
+
*/
|
|
4
|
+
import type { SessionState } from "./types";
|
|
5
|
+
import { readJSON, writeJSON } from "./storage";
|
|
6
|
+
|
|
7
|
+
const SESSION_FILE = "session.json";
|
|
8
|
+
|
|
9
|
+
export function saveSession(state: SessionState): void {
|
|
10
|
+
try {
|
|
11
|
+
writeJSON(SESSION_FILE, state);
|
|
12
|
+
} catch (err) {
|
|
13
|
+
if (process.env.DEBUG)
|
|
14
|
+
console.error(
|
|
15
|
+
`[ghfind] Failed to save session: ${err instanceof Error ? err.message : String(err)}`,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function restoreSession(): SessionState | null {
|
|
21
|
+
return readJSON<SessionState | null>(SESSION_FILE, null);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function clearSession(): void {
|
|
25
|
+
writeJSON(SESSION_FILE, null);
|
|
26
|
+
}
|
package/src/share.ts
CHANGED
|
@@ -1,54 +1,61 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Share repo — copy formatted snippets to clipboard.
|
|
3
|
-
*/
|
|
4
|
-
import type { Repo } from "./types";
|
|
5
|
-
|
|
6
|
-
export type ShareFormat = "markdown" | "plain" | "gh-cli" | "short";
|
|
7
|
-
|
|
8
|
-
export function formatShare(repo: Repo, format: ShareFormat): string {
|
|
9
|
-
switch (format) {
|
|
10
|
-
case "markdown":
|
|
11
|
-
return `[${repo.fullName}](${repo.url}) — ${repo.description ?? ""}`.trim();
|
|
12
|
-
case "plain":
|
|
13
|
-
return `${repo.fullName} — ${repo.description ?? ""} — ${repo.url}`.trim();
|
|
14
|
-
case "gh-cli":
|
|
15
|
-
return `gh repo view ${repo.fullName}`;
|
|
16
|
-
case "short":
|
|
17
|
-
return repo.fullName;
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/** Copy text to clipboard via platform command. */
|
|
22
|
-
import { spawn } from "child_process";
|
|
23
|
-
|
|
24
|
-
export async function copyToClipboard(text: string): Promise<boolean> {
|
|
25
|
-
try {
|
|
26
|
-
const p = process.platform;
|
|
27
|
-
const cmd =
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Share repo — copy formatted snippets to clipboard.
|
|
3
|
+
*/
|
|
4
|
+
import type { Repo } from "./types";
|
|
5
|
+
|
|
6
|
+
export type ShareFormat = "markdown" | "plain" | "gh-cli" | "short";
|
|
7
|
+
|
|
8
|
+
export function formatShare(repo: Repo, format: ShareFormat): string {
|
|
9
|
+
switch (format) {
|
|
10
|
+
case "markdown":
|
|
11
|
+
return `[${repo.fullName}](${repo.url}) — ${repo.description ?? ""}`.trim();
|
|
12
|
+
case "plain":
|
|
13
|
+
return `${repo.fullName} — ${repo.description ?? ""} — ${repo.url}`.trim();
|
|
14
|
+
case "gh-cli":
|
|
15
|
+
return `gh repo view ${repo.fullName}`;
|
|
16
|
+
case "short":
|
|
17
|
+
return repo.fullName;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Copy text to clipboard via platform command. */
|
|
22
|
+
import { spawn } from "child_process";
|
|
23
|
+
|
|
24
|
+
export async function copyToClipboard(text: string): Promise<boolean> {
|
|
25
|
+
try {
|
|
26
|
+
const p = process.platform;
|
|
27
|
+
const cmd =
|
|
28
|
+
p === "win32"
|
|
29
|
+
? ["clip"]
|
|
30
|
+
: p === "darwin"
|
|
31
|
+
? ["pbcopy"]
|
|
32
|
+
: await findLinuxClipboard();
|
|
33
|
+
if (!cmd) return false;
|
|
34
|
+
const proc = spawn(cmd[0], cmd.slice(1), {
|
|
35
|
+
stdio: ["pipe", "ignore", "ignore"],
|
|
36
|
+
});
|
|
37
|
+
proc.stdin.write(text);
|
|
38
|
+
proc.stdin.end();
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
proc.on("close", (code) => resolve(code === 0));
|
|
41
|
+
proc.on("error", () => resolve(false));
|
|
42
|
+
});
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function findLinuxClipboard(): Promise<string[] | null> {
|
|
49
|
+
const { execFileSync } = await import("child_process");
|
|
50
|
+
try {
|
|
51
|
+
execFileSync("xclip", ["-version"], { stdio: "ignore" });
|
|
52
|
+
return ["xclip", "-selection", "clipboard"];
|
|
53
|
+
} catch {
|
|
54
|
+
try {
|
|
55
|
+
execFileSync("wl-copy", ["--version"], { stdio: "ignore" });
|
|
56
|
+
return ["wl-copy"];
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
package/src/status.ts
CHANGED
|
@@ -1,61 +1,68 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Status bar state machine with spinner animation.
|
|
3
|
-
*/
|
|
4
|
-
export type StatusType = "idle" | "searching" | "loading" | "success" | "error";
|
|
5
|
-
|
|
6
|
-
const SEARCH_SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
7
|
-
const LOADING_SPINNER = ["▰▱▱▱▱", "▰▰▱▱▱", "▰▰▰▱▱", "▰▰▰▰▱", "▰▰▰▰▰"];
|
|
8
|
-
|
|
9
|
-
export class StatusManager {
|
|
10
|
-
private spinnerInterval: ReturnType<typeof setInterval> | null = null;
|
|
11
|
-
private spinnerIndex = 0;
|
|
12
|
-
private type: StatusType = "idle";
|
|
13
|
-
private message = "";
|
|
14
|
-
private onUpdate: (text: string) => void;
|
|
15
|
-
|
|
16
|
-
constructor(onUpdate: (text: string) => void) {
|
|
17
|
-
this.onUpdate = onUpdate;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
set(type: StatusType, message: string): void {
|
|
21
|
-
this.stopSpinner();
|
|
22
|
-
this.type = type;
|
|
23
|
-
this.message = message;
|
|
24
|
-
|
|
25
|
-
if (type === "searching" || type === "loading") {
|
|
26
|
-
this.startSpinner(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
this.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Status bar state machine with spinner animation.
|
|
3
|
+
*/
|
|
4
|
+
export type StatusType = "idle" | "searching" | "loading" | "success" | "error";
|
|
5
|
+
|
|
6
|
+
const SEARCH_SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
7
|
+
const LOADING_SPINNER = ["▰▱▱▱▱", "▰▰▱▱▱", "▰▰▰▱▱", "▰▰▰▰▱", "▰▰▰▰▰"];
|
|
8
|
+
|
|
9
|
+
export class StatusManager {
|
|
10
|
+
private spinnerInterval: ReturnType<typeof setInterval> | null = null;
|
|
11
|
+
private spinnerIndex = 0;
|
|
12
|
+
private type: StatusType = "idle";
|
|
13
|
+
private message = "";
|
|
14
|
+
private onUpdate: (text: string) => void;
|
|
15
|
+
|
|
16
|
+
constructor(onUpdate: (text: string) => void) {
|
|
17
|
+
this.onUpdate = onUpdate;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
set(type: StatusType, message: string): void {
|
|
21
|
+
this.stopSpinner();
|
|
22
|
+
this.type = type;
|
|
23
|
+
this.message = message;
|
|
24
|
+
|
|
25
|
+
if (type === "searching" || type === "loading") {
|
|
26
|
+
this.startSpinner(
|
|
27
|
+
type === "searching" ? SEARCH_SPINNER : LOADING_SPINNER,
|
|
28
|
+
);
|
|
29
|
+
} else {
|
|
30
|
+
this.emit();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
private startSpinner(frames: string[]): void {
|
|
35
|
+
this.spinnerIndex = 0;
|
|
36
|
+
this.emit(frames[0]);
|
|
37
|
+
this.spinnerInterval = setInterval(() => {
|
|
38
|
+
this.spinnerIndex = (this.spinnerIndex + 1) % frames.length;
|
|
39
|
+
this.emit(frames[this.spinnerIndex]);
|
|
40
|
+
}, 120);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private stopSpinner(): void {
|
|
44
|
+
if (this.spinnerInterval) {
|
|
45
|
+
clearInterval(this.spinnerInterval);
|
|
46
|
+
this.spinnerInterval = null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private emit(spinner?: string): void {
|
|
51
|
+
const icon =
|
|
52
|
+
this.type === "searching"
|
|
53
|
+
? "🔍"
|
|
54
|
+
: this.type === "loading"
|
|
55
|
+
? "📦"
|
|
56
|
+
: this.type === "success"
|
|
57
|
+
? "✓"
|
|
58
|
+
: this.type === "error"
|
|
59
|
+
? "⚠"
|
|
60
|
+
: " ";
|
|
61
|
+
const prefix = spinner ?? "";
|
|
62
|
+
this.onUpdate(`${icon} ${this.message} ${prefix}`.trim());
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
dispose(): void {
|
|
66
|
+
this.stopSpinner();
|
|
67
|
+
}
|
|
68
|
+
}
|
package/src/storage.ts
CHANGED
|
@@ -1,106 +1,136 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Storage adapter — single point of file I/O for state files.
|
|
3
|
-
*
|
|
4
|
-
* All domain modules (history, bookmarks, saved-searches, notifications,
|
|
5
|
-
* session, config) delegate file I/O here. Swap for an in-memory
|
|
6
|
-
* implementation in tests.
|
|
7
|
-
*/
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Storage adapter — single point of file I/O for state files.
|
|
3
|
+
*
|
|
4
|
+
* All domain modules (history, bookmarks, saved-searches, notifications,
|
|
5
|
+
* session, config) delegate file I/O here. Swap for an in-memory
|
|
6
|
+
* implementation in tests.
|
|
7
|
+
*/
|
|
8
|
+
import {
|
|
9
|
+
existsSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
writeFileSync,
|
|
13
|
+
appendFileSync,
|
|
14
|
+
unlinkSync,
|
|
15
|
+
} from "fs";
|
|
16
|
+
import { join } from "path";
|
|
17
|
+
import { stateDir } from "./config";
|
|
18
|
+
|
|
19
|
+
/** Ensure the state directory exists. */
|
|
20
|
+
function ensureDir(): void {
|
|
21
|
+
mkdirSync(stateDir(), { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Read and parse a JSON file. Returns `fallback` on any error. */
|
|
25
|
+
export function readJSON<T>(filename: string, fallback: T): T {
|
|
26
|
+
ensureDir();
|
|
27
|
+
try {
|
|
28
|
+
return JSON.parse(readFileSync(join(stateDir(), filename), "utf-8")) as T;
|
|
29
|
+
} catch {
|
|
30
|
+
return fallback;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Write data as JSON to a file. */
|
|
35
|
+
export function writeJSON(filename: string, data: unknown): void {
|
|
36
|
+
ensureDir();
|
|
37
|
+
writeFileSync(join(stateDir(), filename), JSON.stringify(data, null, 2));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Append one JSON object as a line to a JSONL file. */
|
|
41
|
+
export function appendJSONL(filename: string, obj: unknown): void {
|
|
42
|
+
ensureDir();
|
|
43
|
+
try {
|
|
44
|
+
appendFileSync(
|
|
45
|
+
join(stateDir(), filename),
|
|
46
|
+
JSON.stringify(obj) + "\n",
|
|
47
|
+
"utf-8",
|
|
48
|
+
);
|
|
49
|
+
} catch {
|
|
50
|
+
// non-critical
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Read all lines from a JSONL file, parsing each as JSON. */
|
|
55
|
+
export function readJSONL<T>(filename: string): T[] {
|
|
56
|
+
ensureDir();
|
|
57
|
+
try {
|
|
58
|
+
const raw = readFileSync(join(stateDir(), filename), "utf-8").trim();
|
|
59
|
+
if (!raw) return [];
|
|
60
|
+
return raw
|
|
61
|
+
.split("\n")
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.map((l) => JSON.parse(l) as T);
|
|
64
|
+
} catch {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Write raw text to a file (bypasses JSON encoding). */
|
|
70
|
+
export function writeRaw(filename: string, data: string): void {
|
|
71
|
+
ensureDir();
|
|
72
|
+
writeFileSync(join(stateDir(), filename), data);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Delete a file if it exists. */
|
|
76
|
+
export function deleteFile(filename: string): void {
|
|
77
|
+
try {
|
|
78
|
+
unlinkSync(join(stateDir(), filename));
|
|
79
|
+
} catch {
|
|
80
|
+
// non-critical
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Append a timestamped debug line to ghfind-debug.log (only when DEBUG is set). */
|
|
85
|
+
export function debugLog(msg: string): void {
|
|
86
|
+
if (!process.env.DEBUG) return;
|
|
87
|
+
try {
|
|
88
|
+
appendFileSync(
|
|
89
|
+
join(stateDir(), "ghfind-debug.log"),
|
|
90
|
+
`[${new Date().toISOString()}] ${msg}\n`,
|
|
91
|
+
"utf-8",
|
|
92
|
+
);
|
|
93
|
+
} catch {
|
|
94
|
+
/* ignore */
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Check if a file exists in the state dir. */
|
|
99
|
+
export function fileExists(filename: string): boolean {
|
|
100
|
+
return existsSync(join(stateDir(), filename));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ─── In-memory implementation for tests ────────────────────────────────
|
|
104
|
+
|
|
105
|
+
const memStore = new Map<string, string>();
|
|
106
|
+
|
|
107
|
+
export const memoryStorage = {
|
|
108
|
+
readJSON<T>(filename: string, fallback: T): T {
|
|
109
|
+
const raw = memStore.get(filename);
|
|
110
|
+
if (!raw) return fallback;
|
|
111
|
+
try {
|
|
112
|
+
return JSON.parse(raw) as T;
|
|
113
|
+
} catch {
|
|
114
|
+
return fallback;
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
writeJSON(filename: string, data: unknown): void {
|
|
118
|
+
memStore.set(filename, JSON.stringify(data, null, 2));
|
|
119
|
+
},
|
|
120
|
+
appendJSONL(filename: string, obj: unknown): void {
|
|
121
|
+
const existing = memStore.get(filename) ?? "";
|
|
122
|
+
memStore.set(filename, existing + JSON.stringify(obj) + "\n");
|
|
123
|
+
},
|
|
124
|
+
readJSONL<T>(filename: string): T[] {
|
|
125
|
+
const raw = memStore.get(filename);
|
|
126
|
+
if (!raw) return [];
|
|
127
|
+
return raw
|
|
128
|
+
.trim()
|
|
129
|
+
.split("\n")
|
|
130
|
+
.filter(Boolean)
|
|
131
|
+
.map((l) => JSON.parse(l) as T);
|
|
132
|
+
},
|
|
133
|
+
clear(): void {
|
|
134
|
+
memStore.clear();
|
|
135
|
+
},
|
|
136
|
+
};
|