pi-readseek 0.3.2 → 0.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 +30 -18
- package/index.ts +16 -136
- package/package.json +4 -4
- package/src/confusable-hyphens.ts +6 -0
- package/src/edit-classify.ts +0 -2
- package/src/edit-diff.ts +3 -6
- package/src/edit-output.ts +1 -7
- package/src/edit.ts +3 -16
- package/src/grep-output.ts +0 -26
- package/src/grep.ts +1 -13
- package/src/hashline.ts +1 -10
- package/src/read-output.ts +1 -27
- package/src/read.ts +1 -10
- package/src/readseek-client.ts +1 -1
- package/src/readseek-settings.ts +6 -134
- package/src/sg-output.ts +0 -30
- package/src/sg.ts +14 -22
- package/src/tool-prompt-metadata.ts +2 -8
- package/src/write.ts +1 -13
- package/prompts/find.md +0 -18
- package/prompts/ls.md +0 -11
- package/src/context-application.ts +0 -70
- package/src/context-hygiene.ts +0 -512
- package/src/doom-loop-suggestions.ts +0 -42
- package/src/doom-loop.ts +0 -216
- package/src/find.ts +0 -613
- package/src/ls.ts +0 -293
- package/src/readseek-command.ts +0 -155
- package/src/readseek-repo.ts +0 -50
package/src/ls.ts
DELETED
|
@@ -1,293 +0,0 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { Text } from "@earendil-works/pi-tui";
|
|
3
|
-
import { Type } from "@sinclair/typebox";
|
|
4
|
-
import { defineToolPromptMetadata } from "./tool-prompt-metadata.js";
|
|
5
|
-
import { readdir, stat } from "node:fs/promises";
|
|
6
|
-
import { resolveToCwd } from "./path-utils.js";
|
|
7
|
-
import { buildReadseekError } from "./readseek-value.js";
|
|
8
|
-
import { coerceObviousBase10Int } from "./coerce-obvious-int.js";
|
|
9
|
-
import { clampLineToWidth, clampLinesToWidth, isRendererExpanded, linkToolPath, renderToolLabel, summaryLine } from "./tui-render-utils.js";
|
|
10
|
-
|
|
11
|
-
const MAX_BYTES = 50 * 1024; // 50 KB
|
|
12
|
-
const DEFAULT_LIMIT = 500;
|
|
13
|
-
|
|
14
|
-
const LS_PROMPT_METADATA = defineToolPromptMetadata({
|
|
15
|
-
promptUrl: new URL("../prompts/ls.md", import.meta.url),
|
|
16
|
-
promptSnippet: "List one directory with directories first and dotfiles included",
|
|
17
|
-
promptGuidelines: [
|
|
18
|
-
"Use ls to inspect one directory; use find for recursive discovery.",
|
|
19
|
-
"Use ls glob to narrow a single-directory listing.",
|
|
20
|
-
"Use read, not ls, for file contents.",
|
|
21
|
-
],
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
export const LS_READSEEK = {
|
|
25
|
-
callable: true,
|
|
26
|
-
enabled: true,
|
|
27
|
-
policy: "read-only" as const,
|
|
28
|
-
readOnly: true,
|
|
29
|
-
pythonName: "ls",
|
|
30
|
-
defaultExposure: "safe-by-default" as const,
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export interface LsEntry {
|
|
34
|
-
name: string;
|
|
35
|
-
type: "file" | "dir";
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface LsReadseekValue {
|
|
39
|
-
tool: "ls";
|
|
40
|
-
path: string;
|
|
41
|
-
totalEntries: number;
|
|
42
|
-
truncated: boolean;
|
|
43
|
-
entries: LsEntry[];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function sortEntries(entries: LsEntry[]): LsEntry[] {
|
|
47
|
-
const dirs = entries.filter((e) => e.type === "dir");
|
|
48
|
-
const files = entries.filter((e) => e.type === "file");
|
|
49
|
-
const cmp = (a: LsEntry, b: LsEntry) => {
|
|
50
|
-
const lower = a.name.toLowerCase().localeCompare(b.name.toLowerCase());
|
|
51
|
-
return lower !== 0 ? lower : a.name.localeCompare(b.name);
|
|
52
|
-
};
|
|
53
|
-
dirs.sort(cmp);
|
|
54
|
-
files.sort(cmp);
|
|
55
|
-
return [...dirs, ...files];
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function formatOutput(entries: LsEntry[], totalCount: number, truncated: boolean): string {
|
|
59
|
-
const lines: string[] = [];
|
|
60
|
-
for (const e of entries) {
|
|
61
|
-
lines.push(e.type === "dir" ? `${e.name}/` : e.name);
|
|
62
|
-
}
|
|
63
|
-
if (truncated) {
|
|
64
|
-
const remaining = totalCount - entries.length;
|
|
65
|
-
lines.push(`[… ${remaining} more entries — use glob to narrow results]`);
|
|
66
|
-
}
|
|
67
|
-
if (entries.length === 0 && !truncated) {
|
|
68
|
-
return "(empty directory)";
|
|
69
|
-
}
|
|
70
|
-
let text = lines.join("\n");
|
|
71
|
-
const bytes = Buffer.byteLength(text, "utf8");
|
|
72
|
-
if (bytes > MAX_BYTES) {
|
|
73
|
-
text = Buffer.from(text, "utf8").subarray(0, MAX_BYTES).toString("utf8") + "\n[… truncated at 50 KB]";
|
|
74
|
-
}
|
|
75
|
-
return text;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function validateGlobBalance(glob: string): string | null {
|
|
79
|
-
let brackets = 0;
|
|
80
|
-
let braces = 0;
|
|
81
|
-
for (let i = 0; i < glob.length; i++) {
|
|
82
|
-
const ch = glob[i];
|
|
83
|
-
if (ch === "\\") {
|
|
84
|
-
i++;
|
|
85
|
-
continue;
|
|
86
|
-
}
|
|
87
|
-
if (ch === "[") brackets++;
|
|
88
|
-
else if (ch === "]") {
|
|
89
|
-
if (brackets === 0) return "Unmatched ']'.";
|
|
90
|
-
brackets--;
|
|
91
|
-
} else if (ch === "{") braces++;
|
|
92
|
-
else if (ch === "}") {
|
|
93
|
-
if (braces === 0) return "Unmatched '}'.";
|
|
94
|
-
braces--;
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
if (brackets !== 0) return "Unterminated character class.";
|
|
98
|
-
if (braces !== 0) return "Unterminated brace expansion.";
|
|
99
|
-
return null;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export function registerLsTool(pi: ExtensionAPI) {
|
|
103
|
-
const tool: Parameters<ExtensionAPI["registerTool"]>[0] & { ptc: typeof LS_READSEEK } = {
|
|
104
|
-
name: "ls",
|
|
105
|
-
label: "ls",
|
|
106
|
-
description: LS_PROMPT_METADATA.description,
|
|
107
|
-
promptSnippet: LS_PROMPT_METADATA.promptSnippet,
|
|
108
|
-
promptGuidelines: LS_PROMPT_METADATA.promptGuidelines,
|
|
109
|
-
ptc: LS_READSEEK,
|
|
110
|
-
parameters: Type.Object({
|
|
111
|
-
path: Type.Optional(Type.String({ description: "Directory path" })),
|
|
112
|
-
limit: Type.Optional(
|
|
113
|
-
Type.Union(
|
|
114
|
-
[Type.Number(), Type.String()],
|
|
115
|
-
{ description: "Max entries" },
|
|
116
|
-
),
|
|
117
|
-
),
|
|
118
|
-
glob: Type.Optional(Type.String({ description: "Glob filter" })),
|
|
119
|
-
}),
|
|
120
|
-
async execute(
|
|
121
|
-
_toolCallId: string,
|
|
122
|
-
params: { path?: string; limit?: number | string; glob?: string },
|
|
123
|
-
_signal: AbortSignal | undefined,
|
|
124
|
-
_onUpdate: any,
|
|
125
|
-
ctx: any,
|
|
126
|
-
) {
|
|
127
|
-
const cwd: string = ctx?.cwd ?? process.cwd();
|
|
128
|
-
const targetPath = params.path ? resolveToCwd(params.path, cwd) : cwd;
|
|
129
|
-
const limitCoerced = coerceObviousBase10Int(params.limit, "limit");
|
|
130
|
-
if (!limitCoerced.ok) {
|
|
131
|
-
return {
|
|
132
|
-
content: [{ type: "text" as const, text: limitCoerced.message }],
|
|
133
|
-
isError: true,
|
|
134
|
-
details: {
|
|
135
|
-
readseekValue: {
|
|
136
|
-
tool: "ls" as const,
|
|
137
|
-
ok: false,
|
|
138
|
-
path: params.path ?? targetPath,
|
|
139
|
-
error: buildReadseekError("invalid-limit", limitCoerced.message),
|
|
140
|
-
},
|
|
141
|
-
},
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
if (limitCoerced.value !== undefined && limitCoerced.value < 1) {
|
|
145
|
-
const message = `Invalid limit: expected a positive integer, received ${limitCoerced.value}.`;
|
|
146
|
-
return {
|
|
147
|
-
content: [{ type: "text" as const, text: message }],
|
|
148
|
-
isError: true,
|
|
149
|
-
details: {
|
|
150
|
-
readseekValue: {
|
|
151
|
-
tool: "ls" as const,
|
|
152
|
-
ok: false,
|
|
153
|
-
path: params.path ?? targetPath,
|
|
154
|
-
error: buildReadseekError("invalid-limit", message),
|
|
155
|
-
},
|
|
156
|
-
},
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
const limit = limitCoerced.value ?? DEFAULT_LIMIT;
|
|
160
|
-
|
|
161
|
-
// Check if path exists and is a directory
|
|
162
|
-
let pathStat;
|
|
163
|
-
try {
|
|
164
|
-
pathStat = await stat(targetPath);
|
|
165
|
-
} catch (err: any) {
|
|
166
|
-
const target = params.path ?? targetPath;
|
|
167
|
-
const code =
|
|
168
|
-
err?.code === "EACCES" || err?.code === "EPERM"
|
|
169
|
-
? "permission-denied"
|
|
170
|
-
: err?.code === "ENOENT"
|
|
171
|
-
? "path-not-found"
|
|
172
|
-
: "fs-error";
|
|
173
|
-
const message =
|
|
174
|
-
code === "permission-denied"
|
|
175
|
-
? `Error: permission denied for path '${target}'`
|
|
176
|
-
: code === "path-not-found"
|
|
177
|
-
? `Error: path '${target}' does not exist`
|
|
178
|
-
: `Error: could not access path '${target}': ${err?.message ?? String(err)}`;
|
|
179
|
-
return {
|
|
180
|
-
content: [{ type: "text" as const, text: message }],
|
|
181
|
-
isError: true,
|
|
182
|
-
details: {
|
|
183
|
-
readseekValue: {
|
|
184
|
-
tool: "ls",
|
|
185
|
-
ok: false,
|
|
186
|
-
path: target,
|
|
187
|
-
error: buildReadseekError(code, message, undefined, code === "fs-error"
|
|
188
|
-
? { fsCode: err?.code, fsMessage: err?.message }
|
|
189
|
-
: undefined),
|
|
190
|
-
},
|
|
191
|
-
},
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
if (!pathStat.isDirectory()) {
|
|
195
|
-
const message = `Error: '${params.path ?? targetPath}' is a file, not a directory. Use read to inspect files.`;
|
|
196
|
-
return {
|
|
197
|
-
content: [{ type: "text" as const, text: message }],
|
|
198
|
-
isError: true,
|
|
199
|
-
details: {
|
|
200
|
-
readseekValue: {
|
|
201
|
-
tool: "ls",
|
|
202
|
-
ok: false,
|
|
203
|
-
path: params.path ?? targetPath,
|
|
204
|
-
error: buildReadseekError(
|
|
205
|
-
"path-not-directory",
|
|
206
|
-
message,
|
|
207
|
-
`Use read(${JSON.stringify(params.path ?? targetPath)}) to inspect files.`,
|
|
208
|
-
),
|
|
209
|
-
},
|
|
210
|
-
},
|
|
211
|
-
};
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
// Read directory
|
|
215
|
-
const dirents = await readdir(targetPath, { withFileTypes: true });
|
|
216
|
-
let allEntries: LsEntry[] = dirents.map((d) => ({
|
|
217
|
-
name: d.name,
|
|
218
|
-
type: d.isDirectory() ? ("dir" as const) : ("file" as const),
|
|
219
|
-
}));
|
|
220
|
-
|
|
221
|
-
// Apply glob filter
|
|
222
|
-
if (params.glob) {
|
|
223
|
-
const balanceError = validateGlobBalance(params.glob);
|
|
224
|
-
if (balanceError) {
|
|
225
|
-
const message = `Invalid glob ${JSON.stringify(params.glob)}: ${balanceError}`;
|
|
226
|
-
return {
|
|
227
|
-
content: [{ type: "text" as const, text: message }],
|
|
228
|
-
isError: true,
|
|
229
|
-
details: {
|
|
230
|
-
readseekValue: {
|
|
231
|
-
tool: "ls" as const,
|
|
232
|
-
ok: false,
|
|
233
|
-
path: params.path ?? targetPath,
|
|
234
|
-
error: buildReadseekError("invalid-params-combo", message),
|
|
235
|
-
},
|
|
236
|
-
},
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
const picomatch = (await import("picomatch" as any)).default;
|
|
240
|
-
const isMatch = picomatch(params.glob);
|
|
241
|
-
allEntries = allEntries.filter((e) => isMatch(e.name));
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Sort: dirs first, then files, each group alpha case-insensitive
|
|
245
|
-
const sorted = sortEntries(allEntries);
|
|
246
|
-
const totalCount = sorted.length;
|
|
247
|
-
const truncated = totalCount > limit;
|
|
248
|
-
const displayed = truncated ? sorted.slice(0, limit) : sorted;
|
|
249
|
-
|
|
250
|
-
const text = formatOutput(displayed, totalCount, truncated);
|
|
251
|
-
const readseekValue: LsReadseekValue = {
|
|
252
|
-
tool: "ls",
|
|
253
|
-
path: targetPath,
|
|
254
|
-
totalEntries: totalCount,
|
|
255
|
-
truncated,
|
|
256
|
-
entries: displayed,
|
|
257
|
-
};
|
|
258
|
-
|
|
259
|
-
return {
|
|
260
|
-
content: [{ type: "text" as const, text }],
|
|
261
|
-
details: { readseekValue },
|
|
262
|
-
};
|
|
263
|
-
},
|
|
264
|
-
|
|
265
|
-
renderCall(args: any, theme: any, context: any = {}) {
|
|
266
|
-
const { path } = args as { path?: string };
|
|
267
|
-
const cwd = context.cwd ?? process.cwd();
|
|
268
|
-
const displayPath = path ?? ".";
|
|
269
|
-
const linkedPath = linkToolPath(theme.fg("muted", displayPath), displayPath, cwd);
|
|
270
|
-
return new Text(clampLineToWidth(`${renderToolLabel(theme, "ls")} ${linkedPath}`, context.width), 0, 0);
|
|
271
|
-
},
|
|
272
|
-
|
|
273
|
-
renderResult(result: any, options: any, theme: any, context: any = {}) {
|
|
274
|
-
const expanded = isRendererExpanded(options, context);
|
|
275
|
-
const width = context.width ?? options?.width;
|
|
276
|
-
const output = result.content[0]?.type === "text" ? (result.content[0] as { type: "text"; text: string }).text : "";
|
|
277
|
-
if (result.isError || context.isError) {
|
|
278
|
-
const firstLine = output.split("\n")[0] || "error";
|
|
279
|
-
const body = expanded && output ? output : firstLine;
|
|
280
|
-
return new Text(clampLinesToWidth(summaryLine(body).split("\n"), width).join("\n"), 0, 0);
|
|
281
|
-
}
|
|
282
|
-
const readseekValue = result.details?.readseekValue as { totalEntries?: number } | undefined;
|
|
283
|
-
const total = readseekValue?.totalEntries ?? output.split("\n").filter(Boolean).length;
|
|
284
|
-
if (total === 0) return new Text(summaryLine("no entries"), 0, 0);
|
|
285
|
-
let text = summaryLine(`${total} ${total === 1 ? "entry" : "entries"} returned`, { hidden: !!output && !expanded });
|
|
286
|
-
if (expanded && output) text += `\n${output}`;
|
|
287
|
-
return new Text(clampLinesToWidth(text.split("\n"), width).join("\n"), 0, 0);
|
|
288
|
-
},
|
|
289
|
-
};
|
|
290
|
-
|
|
291
|
-
pi.registerTool(tool);
|
|
292
|
-
return tool;
|
|
293
|
-
}
|
package/src/readseek-command.ts
DELETED
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { Key, matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
|
-
import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { dirname, join, relative } from "node:path";
|
|
5
|
-
import { findReadseekDir, initReadseekDir } from "./readseek-repo.js";
|
|
6
|
-
|
|
7
|
-
type ReadseekAction = "init" | "deinit" | null;
|
|
8
|
-
|
|
9
|
-
function stripFromGitignore(dir: string): void {
|
|
10
|
-
const gitignorePath = join(dir, ".gitignore");
|
|
11
|
-
if (!existsSync(gitignorePath)) return;
|
|
12
|
-
const lines = readFileSync(gitignorePath, "utf-8").split("\n");
|
|
13
|
-
const filtered = lines.filter((line) => line.trim() !== "/.readseek");
|
|
14
|
-
writeFileSync(gitignorePath, filtered.join("\n"), "utf-8");
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
async function deinit(ctx: ExtensionContext): Promise<void> {
|
|
18
|
-
const dir = findReadseekDir(ctx.cwd);
|
|
19
|
-
if (!dir) {
|
|
20
|
-
ctx.ui.notify("No .readseek directory found", "info");
|
|
21
|
-
return;
|
|
22
|
-
}
|
|
23
|
-
const projectDir = dirname(dir);
|
|
24
|
-
rmSync(dir, { recursive: true, force: true });
|
|
25
|
-
stripFromGitignore(projectDir);
|
|
26
|
-
ctx.ui.notify("Removed .readseek/", "info");
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async function init(ctx: ExtensionContext): Promise<void> {
|
|
30
|
-
const projectDir = ctx.cwd;
|
|
31
|
-
if (findReadseekDir(projectDir)) {
|
|
32
|
-
ctx.ui.notify(".readseek/ already exists", "info");
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
initReadseekDir(projectDir);
|
|
36
|
-
ctx.ui.notify("Initialized .readseek/", "info");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function registerReadseekCommand(pi: ExtensionAPI): void {
|
|
40
|
-
const maybePi = pi as ExtensionAPI & {
|
|
41
|
-
registerCommand?: ExtensionAPI["registerCommand"];
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
maybePi.registerCommand?.("readseek", {
|
|
45
|
-
description: "Manage .readseek/ map cache",
|
|
46
|
-
handler: async (_args, ctx) => {
|
|
47
|
-
if (!ctx.hasUI) return;
|
|
48
|
-
|
|
49
|
-
const action = await new Promise<ReadseekAction>((resolve) => {
|
|
50
|
-
const initialized = findReadseekDir(ctx.cwd) !== null;
|
|
51
|
-
|
|
52
|
-
void ctx.ui.custom<ReadseekAction>(
|
|
53
|
-
(tui, theme, _kb, done) => {
|
|
54
|
-
return {
|
|
55
|
-
render(width: number): string[] {
|
|
56
|
-
const innerW = Math.max(1, width - 4);
|
|
57
|
-
const border = theme.fg("border", "│");
|
|
58
|
-
const dim = (s: string) => theme.fg("dim", s);
|
|
59
|
-
const accent = (s: string) => theme.fg("accent", s);
|
|
60
|
-
const borderFg = (s: string) => theme.fg("border", s);
|
|
61
|
-
|
|
62
|
-
function row(content: string): string {
|
|
63
|
-
const line = truncateToWidth(content, innerW);
|
|
64
|
-
const pad = Math.max(0, innerW - visibleWidth(line));
|
|
65
|
-
return `${border} ${line}${" ".repeat(pad)} ${border}`;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const lines: string[] = [];
|
|
69
|
-
|
|
70
|
-
const label = " Readseek ";
|
|
71
|
-
const topFill = borderFg(
|
|
72
|
-
"─".repeat(Math.max(0, width - 4 - visibleWidth(label))),
|
|
73
|
-
);
|
|
74
|
-
lines.push(
|
|
75
|
-
`${borderFg("╭─")}${accent(label)}${topFill}${borderFg("─╮")}`,
|
|
76
|
-
);
|
|
77
|
-
|
|
78
|
-
lines.push(row(""));
|
|
79
|
-
|
|
80
|
-
const readseekDir = findReadseekDir(ctx.cwd);
|
|
81
|
-
const statusDot = initialized
|
|
82
|
-
? theme.fg("success", "●")
|
|
83
|
-
: theme.fg("warning", "●");
|
|
84
|
-
const statusLabel = initialized ? "Initialized" : "Not initialized";
|
|
85
|
-
const statusColor = initialized
|
|
86
|
-
? theme.fg("success", statusLabel)
|
|
87
|
-
: theme.fg("warning", statusLabel);
|
|
88
|
-
const pathText = readseekDir
|
|
89
|
-
? dim(relative(ctx.cwd, readseekDir) || readseekDir)
|
|
90
|
-
: dim(".readseek");
|
|
91
|
-
lines.push(
|
|
92
|
-
row(`${statusDot} ${statusColor} ${dim("·")} ${pathText}`),
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
lines.push(row(""));
|
|
96
|
-
|
|
97
|
-
if (initialized) {
|
|
98
|
-
lines.push(
|
|
99
|
-
row(`${accent("▶")} ${dim("[d]")} Deinit ${dim("remove .readseek/")}`),
|
|
100
|
-
);
|
|
101
|
-
} else {
|
|
102
|
-
lines.push(
|
|
103
|
-
row(
|
|
104
|
-
`${accent("▶")} ${dim("[i]")} Init ${dim("create .readseek/ map cache")}`,
|
|
105
|
-
),
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
lines.push(row(""));
|
|
110
|
-
lines.push(row(dim("esc close")));
|
|
111
|
-
|
|
112
|
-
lines.push(
|
|
113
|
-
`${borderFg("╰")}${borderFg("─".repeat(Math.max(0, width - 2)))}${borderFg("╯")}`,
|
|
114
|
-
);
|
|
115
|
-
|
|
116
|
-
return lines;
|
|
117
|
-
},
|
|
118
|
-
|
|
119
|
-
handleInput(data: string): void {
|
|
120
|
-
if (matchesKey(data, Key.escape) || matchesKey(data, Key.ctrl("c"))) {
|
|
121
|
-
done(null);
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (!initialized && (data === "i" || data === "I")) {
|
|
126
|
-
done("init");
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
if (initialized && (data === "d" || data === "D")) {
|
|
130
|
-
done("deinit");
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
},
|
|
134
|
-
|
|
135
|
-
invalidate(): void {},
|
|
136
|
-
};
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
overlay: true,
|
|
140
|
-
overlayOptions: {
|
|
141
|
-
anchor: "center",
|
|
142
|
-
width: 60,
|
|
143
|
-
margin: 2,
|
|
144
|
-
},
|
|
145
|
-
},
|
|
146
|
-
).then((result) => {
|
|
147
|
-
resolve(result ?? null);
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
if (action === "init") await init(ctx);
|
|
152
|
-
else if (action === "deinit") await deinit(ctx);
|
|
153
|
-
},
|
|
154
|
-
});
|
|
155
|
-
}
|
package/src/readseek-repo.ts
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
|
-
import { dirname, join, resolve } from "node:path";
|
|
3
|
-
|
|
4
|
-
export function findReadseekDir(cwd: string): string | null {
|
|
5
|
-
const abs = resolve(cwd);
|
|
6
|
-
for (let dir = abs; ; dir = dirname(dir)) {
|
|
7
|
-
const candidate = join(dir, ".readseek");
|
|
8
|
-
if (existsSync(candidate)) return candidate;
|
|
9
|
-
const parent = dirname(dir);
|
|
10
|
-
if (parent === dir) break;
|
|
11
|
-
}
|
|
12
|
-
return null;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function hasReadseekDir(cwd: string): boolean {
|
|
16
|
-
return findReadseekDir(cwd) !== null;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function initReadseekDir(cwd: string): string {
|
|
20
|
-
const abs = resolve(cwd);
|
|
21
|
-
const readseekDir = join(abs, ".readseek");
|
|
22
|
-
const mapsDir = join(readseekDir, "maps");
|
|
23
|
-
|
|
24
|
-
if (existsSync(readseekDir)) {
|
|
25
|
-
throw new Error(`.readseek/ already exists in ${abs}`);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
mkdirSync(mapsDir, { recursive: true });
|
|
29
|
-
|
|
30
|
-
const gitignore = join(abs, ".gitignore");
|
|
31
|
-
const entry = "/.readseek";
|
|
32
|
-
const needsAppend = (() => {
|
|
33
|
-
if (!existsSync(gitignore)) return true;
|
|
34
|
-
const contents = readFileSync(gitignore, "utf-8");
|
|
35
|
-
return !contents.split("\n").some((line) => line.trim() === entry);
|
|
36
|
-
})();
|
|
37
|
-
|
|
38
|
-
if (needsAppend) {
|
|
39
|
-
let prefix = "";
|
|
40
|
-
if (existsSync(gitignore)) {
|
|
41
|
-
const contents = readFileSync(gitignore, "utf-8");
|
|
42
|
-
if (contents.length > 0 && !contents.endsWith("\n")) {
|
|
43
|
-
prefix = "\n";
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
appendFileSync(gitignore, `${prefix}${entry}\n`, "utf-8");
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return readseekDir;
|
|
50
|
-
}
|