@heyhuynhgiabuu/pi-pretty 0.6.25 → 0.6.27
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 +34 -0
- package/README.md +47 -17
- package/dist/config.d.ts +16 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +30 -0
- package/dist/config.js.map +1 -1
- package/dist/fff.d.ts +16 -1
- package/dist/fff.d.ts.map +1 -1
- package/dist/fff.js +139 -33
- package/dist/fff.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +192 -43
- package/dist/index.js.map +1 -1
- package/dist/prompt-editor.d.ts +11 -0
- package/dist/prompt-editor.d.ts.map +1 -0
- package/dist/prompt-editor.js +78 -0
- package/dist/prompt-editor.js.map +1 -0
- package/dist/render.d.ts +9 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +25 -9
- package/dist/render.js.map +1 -1
- package/dist/tools/bash.d.ts.map +1 -1
- package/dist/tools/bash.js +73 -30
- package/dist/tools/bash.js.map +1 -1
- package/dist/tools/find.d.ts.map +1 -1
- package/dist/tools/find.js +29 -10
- package/dist/tools/find.js.map +1 -1
- package/dist/tools/grep.d.ts.map +1 -1
- package/dist/tools/grep.js +20 -10
- package/dist/tools/grep.js.map +1 -1
- package/dist/tools/ls.d.ts.map +1 -1
- package/dist/tools/ls.js +10 -5
- package/dist/tools/ls.js.map +1 -1
- package/dist/tools/metrics.d.ts +6 -1
- package/dist/tools/metrics.d.ts.map +1 -1
- package/dist/tools/metrics.js +21 -8
- package/dist/tools/metrics.js.map +1 -1
- package/dist/tools/read.d.ts +2 -1
- package/dist/tools/read.d.ts.map +1 -1
- package/dist/tools/read.js +93 -30
- package/dist/tools/read.js.map +1 -1
- package/dist/types.d.ts +13 -3
- package/dist/types.d.ts.map +1 -1
- package/dist/working-indicator.d.ts +91 -26
- package/dist/working-indicator.d.ts.map +1 -1
- package/dist/working-indicator.js +259 -77
- package/dist/working-indicator.js.map +1 -1
- package/package.json +7 -6
- package/src/config.ts +51 -0
- package/src/fff.ts +158 -29
- package/src/index.ts +207 -47
- package/src/prompt-editor.ts +102 -0
- package/src/render.ts +30 -11
- package/src/tools/bash.ts +94 -32
- package/src/tools/find.ts +39 -17
- package/src/tools/grep.ts +26 -15
- package/src/tools/ls.ts +19 -10
- package/src/tools/metrics.ts +28 -8
- package/src/tools/read.ts +122 -30
- package/src/types.ts +14 -3
- package/src/working-indicator.ts +309 -88
package/src/config.ts
CHANGED
|
@@ -77,6 +77,7 @@ export interface WorkingIndicatorConfig {
|
|
|
77
77
|
mid?: string;
|
|
78
78
|
high?: string;
|
|
79
79
|
bold?: boolean;
|
|
80
|
+
/** @deprecated Retained for config compatibility; the working row no longer renders this hint. */
|
|
80
81
|
hint?: boolean;
|
|
81
82
|
/** Tint the indicator with a stable per-session accent color. */
|
|
82
83
|
sessionAccent?: boolean;
|
|
@@ -86,6 +87,18 @@ export interface ThinkingIndicatorConfig {
|
|
|
86
87
|
enabled?: boolean;
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
export interface FffConfig {
|
|
91
|
+
/** Allow FFF to index the user's home directory when Pi starts there. */
|
|
92
|
+
enableHomeScanning?: boolean;
|
|
93
|
+
/** Allow FFF to index the filesystem root when Pi starts there. */
|
|
94
|
+
enableRootScanning?: boolean;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface FffScanSettings {
|
|
98
|
+
enableHomeScanning: boolean;
|
|
99
|
+
enableRootScanning: boolean;
|
|
100
|
+
}
|
|
101
|
+
|
|
89
102
|
export interface PrettyConfig {
|
|
90
103
|
background?: {
|
|
91
104
|
tool?: string;
|
|
@@ -100,6 +113,7 @@ export interface PrettyConfig {
|
|
|
100
113
|
cacheLimit?: number;
|
|
101
114
|
workingIndicator?: WorkingIndicatorConfig;
|
|
102
115
|
thinkingIndicator?: ThinkingIndicatorConfig;
|
|
116
|
+
fff?: FffConfig;
|
|
103
117
|
}
|
|
104
118
|
|
|
105
119
|
/** Normalize a comma-separated env value or JSON array into tool names. */
|
|
@@ -148,6 +162,14 @@ export function loadConfig(agentDir = getConfigDir()): PrettyConfig {
|
|
|
148
162
|
if (maxHlChars) config.maxHlChars = maxHlChars;
|
|
149
163
|
if (maxPreviewLines) config.maxPreviewLines = maxPreviewLines;
|
|
150
164
|
if (cacheLimit) config.cacheLimit = cacheLimit;
|
|
165
|
+
const fff = parsed.fff;
|
|
166
|
+
if (fff && typeof fff === "object") {
|
|
167
|
+
const fffConfig: FffConfig = {};
|
|
168
|
+
const src = fff as Record<string, unknown>;
|
|
169
|
+
if (typeof src.enableHomeScanning === "boolean") fffConfig.enableHomeScanning = src.enableHomeScanning;
|
|
170
|
+
if (typeof src.enableRootScanning === "boolean") fffConfig.enableRootScanning = src.enableRootScanning;
|
|
171
|
+
if (Object.keys(fffConfig).length > 0) config.fff = fffConfig;
|
|
172
|
+
}
|
|
151
173
|
const workingIndicator = parsed.workingIndicator;
|
|
152
174
|
if (workingIndicator && typeof workingIndicator === "object") {
|
|
153
175
|
const wi: WorkingIndicatorConfig = {};
|
|
@@ -185,6 +207,35 @@ export function getConfigDir(): string | undefined {
|
|
|
185
207
|
return process.env.PRETTY_CONFIG_DIR ?? getDefaultAgentDir();
|
|
186
208
|
}
|
|
187
209
|
|
|
210
|
+
function parseBooleanOverride(value: unknown): boolean | undefined {
|
|
211
|
+
if (typeof value === "boolean") return value;
|
|
212
|
+
if (typeof value !== "string") return undefined;
|
|
213
|
+
const normalized = value.trim().toLowerCase();
|
|
214
|
+
if (normalized === "1" || normalized === "true" || normalized === "on") return true;
|
|
215
|
+
if (normalized === "0" || normalized === "false" || normalized === "off") return false;
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function resolveFffScanSettings(
|
|
220
|
+
config: Pick<PrettyConfig, "fff">,
|
|
221
|
+
flags: {
|
|
222
|
+
enableHomeScanning?: boolean | string;
|
|
223
|
+
enableRootScanning?: boolean | string;
|
|
224
|
+
} = {},
|
|
225
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
226
|
+
): FffScanSettings {
|
|
227
|
+
const resolve = (
|
|
228
|
+
flagValue: boolean | string | undefined,
|
|
229
|
+
envName: string,
|
|
230
|
+
configValue: boolean | undefined,
|
|
231
|
+
): boolean => parseBooleanOverride(flagValue) ?? parseBooleanOverride(env[envName]) ?? configValue ?? false;
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
enableHomeScanning: resolve(flags.enableHomeScanning, "PRETTY_FFF_HOME_SCAN", config.fff?.enableHomeScanning),
|
|
235
|
+
enableRootScanning: resolve(flags.enableRootScanning, "PRETTY_FFF_ROOT_SCAN", config.fff?.enableRootScanning),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
188
239
|
/**
|
|
189
240
|
* Merge env tool sets with config-file tool lists: a non-empty env set wins
|
|
190
241
|
* over the config file (12-factor precedence); otherwise config applies.
|
package/src/fff.ts
CHANGED
|
@@ -6,11 +6,44 @@
|
|
|
6
6
|
* Graceful fallback: if FFF is not installed or fails, tools degrade to SDK.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { mkdirSync } from "node:fs";
|
|
10
|
-
import {
|
|
9
|
+
import { mkdirSync, realpathSync } from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { join, parse, resolve } from "node:path";
|
|
11
12
|
|
|
12
13
|
import type { FileFinder as FffFileFinder, GrepCursor } from "@ff-labs/fff-node";
|
|
13
14
|
|
|
15
|
+
export interface FffInitOptions {
|
|
16
|
+
enableHomeDirScanning: boolean;
|
|
17
|
+
enableFsRootScanning: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type FffInitFailureKind = "restricted-base-path" | "native";
|
|
21
|
+
export type FffRestrictedScanScope = "home" | "root";
|
|
22
|
+
|
|
23
|
+
export class FffInitError extends Error {
|
|
24
|
+
readonly kind: FffInitFailureKind;
|
|
25
|
+
readonly restrictedScope: FffRestrictedScanScope | undefined;
|
|
26
|
+
|
|
27
|
+
constructor(kind: FffInitFailureKind, message: string, restrictedScope?: FffRestrictedScanScope) {
|
|
28
|
+
super(message);
|
|
29
|
+
this.name = "FffInitError";
|
|
30
|
+
this.kind = kind;
|
|
31
|
+
this.restrictedScope = restrictedScope;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function isFffRestrictedBasePathError(error: unknown, options: FffInitOptions): boolean {
|
|
36
|
+
if (!(error instanceof FffInitError) || error.kind !== "restricted-base-path") return false;
|
|
37
|
+
if (error.restrictedScope === "home") return !options.enableHomeDirScanning;
|
|
38
|
+
if (error.restrictedScope === "root") return !options.enableFsRootScanning;
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const DEFAULT_FFF_INIT_OPTIONS: FffInitOptions = {
|
|
43
|
+
enableHomeDirScanning: false,
|
|
44
|
+
enableFsRootScanning: false,
|
|
45
|
+
};
|
|
46
|
+
|
|
14
47
|
// ---------------------------------------------------------------------------
|
|
15
48
|
// Cursor store — pagination cursors for grep
|
|
16
49
|
// ---------------------------------------------------------------------------
|
|
@@ -53,6 +86,12 @@ type SharedFffState = {
|
|
|
53
86
|
service: FffService;
|
|
54
87
|
};
|
|
55
88
|
|
|
89
|
+
type FinderConfig = {
|
|
90
|
+
basePath: string;
|
|
91
|
+
enableHomeDirScanning: boolean;
|
|
92
|
+
enableFsRootScanning: boolean;
|
|
93
|
+
};
|
|
94
|
+
|
|
56
95
|
type GlobalWithFffService = typeof globalThis & {
|
|
57
96
|
[SHARED_FFF_SERVICE_KEY]?: SharedFffState;
|
|
58
97
|
};
|
|
@@ -83,7 +122,9 @@ export class FffService {
|
|
|
83
122
|
|
|
84
123
|
private fffModule: typeof import("@ff-labs/fff-node") | null = null;
|
|
85
124
|
private dbDir: string | null = null;
|
|
125
|
+
private finderConfig: FinderConfig | null = null;
|
|
86
126
|
private finderPromise: Promise<void> | null = null;
|
|
127
|
+
private finderPromiseConfig: FinderConfig | null = null;
|
|
87
128
|
|
|
88
129
|
constructor(fffModule?: typeof import("@ff-labs/fff-node"), agentDir?: string) {
|
|
89
130
|
this.configure(fffModule, agentDir);
|
|
@@ -130,47 +171,97 @@ export class FffService {
|
|
|
130
171
|
}
|
|
131
172
|
}
|
|
132
173
|
|
|
133
|
-
async ensureFinder(cwd: string): Promise<void> {
|
|
134
|
-
|
|
135
|
-
if (this.finderPromise) return this.finderPromise;
|
|
174
|
+
async ensureFinder(cwd: string, options: FffInitOptions = DEFAULT_FFF_INIT_OPTIONS): Promise<void> {
|
|
175
|
+
const requestedConfig = finderConfig(cwd, options);
|
|
136
176
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
177
|
+
for (;;) {
|
|
178
|
+
if (this.finder && !this.finder.isDestroyed && sameFinderConfig(this.finderConfig, requestedConfig)) return;
|
|
179
|
+
|
|
180
|
+
const pending = this.finderPromise;
|
|
181
|
+
if (pending) {
|
|
182
|
+
const pendingConfig = this.finderPromiseConfig;
|
|
183
|
+
try {
|
|
184
|
+
await pending;
|
|
185
|
+
} catch (error) {
|
|
186
|
+
// A concurrent request for the same scope must observe the same
|
|
187
|
+
// failure. A different scope can retry after the pending attempt.
|
|
188
|
+
if (sameFinderConfig(pendingConfig, requestedConfig)) throw error;
|
|
189
|
+
}
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const promise = this._createFinder(cwd, options, requestedConfig);
|
|
194
|
+
this.finderPromise = promise;
|
|
195
|
+
this.finderPromiseConfig = requestedConfig;
|
|
196
|
+
try {
|
|
197
|
+
await promise;
|
|
198
|
+
return;
|
|
199
|
+
} finally {
|
|
200
|
+
if (this.finderPromise === promise) {
|
|
201
|
+
this.finderPromise = null;
|
|
202
|
+
this.finderPromiseConfig = null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
143
205
|
}
|
|
144
206
|
}
|
|
145
207
|
|
|
146
|
-
private async _createFinder(cwd: string): Promise<void> {
|
|
208
|
+
private async _createFinder(cwd: string, options: FffInitOptions, config: FinderConfig): Promise<void> {
|
|
147
209
|
if (!this.fffModule) return;
|
|
148
210
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
211
|
+
const result = this.createFinder(cwd, this.dbDir, options);
|
|
212
|
+
if (!result.ok) {
|
|
213
|
+
const kind = isRestrictedBasePathMessage(result.error) ? "restricted-base-path" : "native";
|
|
214
|
+
throw new FffInitError(
|
|
215
|
+
kind,
|
|
216
|
+
result.error,
|
|
217
|
+
kind === "restricted-base-path" ? restrictedScanScope(cwd) : undefined,
|
|
218
|
+
);
|
|
152
219
|
}
|
|
153
220
|
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
221
|
+
const candidate = result.value;
|
|
222
|
+
let scan: Awaited<ReturnType<typeof candidate.waitForScan>>;
|
|
223
|
+
try {
|
|
224
|
+
scan = await candidate.waitForScan(15_000);
|
|
225
|
+
} catch (error) {
|
|
226
|
+
if (!candidate.isDestroyed) {
|
|
227
|
+
try {
|
|
228
|
+
candidate.destroy();
|
|
229
|
+
} catch {
|
|
230
|
+
// Preserve the scan failure; the candidate is no longer selected.
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
throw error;
|
|
157
234
|
}
|
|
158
235
|
|
|
159
|
-
|
|
160
|
-
|
|
236
|
+
const previous = this.finder;
|
|
237
|
+
this.finder = candidate;
|
|
161
238
|
this.partialIndex = scan.ok && !scan.value;
|
|
239
|
+
this.finderConfig = config;
|
|
240
|
+
if (previous && previous !== candidate && !previous.isDestroyed) {
|
|
241
|
+
try {
|
|
242
|
+
previous.destroy();
|
|
243
|
+
} catch {
|
|
244
|
+
// Keep the replacement active; native cleanup is best effort.
|
|
245
|
+
}
|
|
246
|
+
}
|
|
162
247
|
}
|
|
163
248
|
|
|
164
249
|
private createFinder(
|
|
165
250
|
cwd: string,
|
|
166
251
|
dbDir: string | null,
|
|
252
|
+
options: FffInitOptions,
|
|
167
253
|
): ReturnType<typeof import("@ff-labs/fff-node").FileFinder.create> {
|
|
168
254
|
if (!this.fffModule) throw new Error("FFF module is not loaded");
|
|
169
|
-
const
|
|
255
|
+
const commonOptions = {
|
|
170
256
|
basePath: cwd,
|
|
257
|
+
aiMode: true,
|
|
258
|
+
enableHomeDirScanning: options.enableHomeDirScanning,
|
|
259
|
+
enableFsRootScanning: options.enableFsRootScanning,
|
|
260
|
+
};
|
|
261
|
+
const result = this.fffModule.FileFinder.create({
|
|
262
|
+
...commonOptions,
|
|
171
263
|
frecencyDbPath: dbDir ? join(dbDir, "frecency.mdb") : "",
|
|
172
264
|
historyDbPath: dbDir ? join(dbDir, "history.mdb") : "",
|
|
173
|
-
aiMode: true,
|
|
174
265
|
});
|
|
175
266
|
if (result.ok || !isAlreadyOpenError(result.error) || !dbDir) return result;
|
|
176
267
|
|
|
@@ -181,20 +272,23 @@ export class FffService {
|
|
|
181
272
|
return result;
|
|
182
273
|
}
|
|
183
274
|
return this.fffModule.FileFinder.create({
|
|
184
|
-
|
|
275
|
+
...commonOptions,
|
|
185
276
|
frecencyDbPath: join(isolatedDir, "frecency.mdb"),
|
|
186
277
|
historyDbPath: join(isolatedDir, "history.mdb"),
|
|
187
|
-
aiMode: true,
|
|
188
278
|
});
|
|
189
279
|
}
|
|
190
280
|
|
|
191
281
|
destroy(): void {
|
|
192
|
-
|
|
193
|
-
this.finder.destroy();
|
|
194
|
-
this.finder = null;
|
|
195
|
-
}
|
|
196
|
-
this.partialIndex = false;
|
|
282
|
+
this.disposeFinder();
|
|
197
283
|
this.finderPromise = null;
|
|
284
|
+
this.finderPromiseConfig = null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private disposeFinder(): void {
|
|
288
|
+
if (this.finder && !this.finder.isDestroyed) this.finder.destroy();
|
|
289
|
+
this.finder = null;
|
|
290
|
+
this.finderConfig = null;
|
|
291
|
+
this.partialIndex = false;
|
|
198
292
|
}
|
|
199
293
|
|
|
200
294
|
getFinder(): import("@ff-labs/fff-node").FileFinder | null {
|
|
@@ -206,6 +300,41 @@ export class FffService {
|
|
|
206
300
|
}
|
|
207
301
|
}
|
|
208
302
|
|
|
303
|
+
function finderConfig(cwd: string, options: FffInitOptions): FinderConfig {
|
|
304
|
+
return {
|
|
305
|
+
basePath: canonicalPath(cwd),
|
|
306
|
+
enableHomeDirScanning: options.enableHomeDirScanning,
|
|
307
|
+
enableFsRootScanning: options.enableFsRootScanning,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function sameFinderConfig(left: FinderConfig | null, right: FinderConfig): boolean {
|
|
312
|
+
return (
|
|
313
|
+
left?.basePath === right.basePath &&
|
|
314
|
+
left.enableHomeDirScanning === right.enableHomeDirScanning &&
|
|
315
|
+
left.enableFsRootScanning === right.enableFsRootScanning
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
209
319
|
function isAlreadyOpenError(error: string): boolean {
|
|
210
320
|
return /environment already open/i.test(error);
|
|
211
321
|
}
|
|
322
|
+
|
|
323
|
+
function isRestrictedBasePathMessage(error: string): boolean {
|
|
324
|
+
return /certain FFF features in a file system root or home directories/i.test(error);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function restrictedScanScope(cwd: string): FffRestrictedScanScope | undefined {
|
|
328
|
+
const basePath = canonicalPath(cwd);
|
|
329
|
+
if (basePath === parse(basePath).root) return "root";
|
|
330
|
+
if (basePath === canonicalPath(homedir())) return "home";
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function canonicalPath(path: string): string {
|
|
335
|
+
try {
|
|
336
|
+
return realpathSync.native(path);
|
|
337
|
+
} catch {
|
|
338
|
+
return resolve(path);
|
|
339
|
+
}
|
|
340
|
+
}
|