@narumitw/pi-btw 0.58.1 → 0.59.0
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 +2 -0
- package/dist/index.ts +284 -365
- package/dist/index.ts.map +3 -3
- package/package.json +52 -53
- package/src/bring-to-main.ts +494 -547
- package/src/btw.ts +814 -856
- package/src/fullscreen-ui.ts +673 -674
- package/src/keybindings.ts +228 -270
- package/src/main-tree-picker.ts +309 -318
- package/src/menu.ts +416 -454
- package/src/settings.ts +203 -219
- package/src/side-thread.ts +173 -201
- package/src/text.ts +21 -23
- package/src/transcript-markdown.ts +65 -0
- package/src/transcript-pager.ts +611 -662
package/src/settings.ts
CHANGED
|
@@ -3,11 +3,7 @@ import { constants } from "node:fs";
|
|
|
3
3
|
import { mkdir, open, rename, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { basename, dirname, join } from "node:path";
|
|
5
5
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import {
|
|
7
|
-
BTW_SHORTCUT_ACTIONS,
|
|
8
|
-
type BtwKeybindingOverrides,
|
|
9
|
-
normalizeBtwKey,
|
|
10
|
-
} from "./keybindings.js";
|
|
6
|
+
import { BTW_SHORTCUT_ACTIONS, type BtwKeybindingOverrides, normalizeBtwKey } from "./keybindings.js";
|
|
11
7
|
import { BTW_THINKING_LEVELS, type BtwThinkingLevel } from "./side-thread.js";
|
|
12
8
|
|
|
13
9
|
export const BTW_SETTINGS_FILE = "pi-btw.json";
|
|
@@ -16,31 +12,31 @@ export const DEFAULT_REMEMBER_THINKING_LEVEL_CHANGES = true;
|
|
|
16
12
|
const MAX_SETTINGS_BYTES = 64 * 1024;
|
|
17
13
|
|
|
18
14
|
export interface BtwSettings {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
15
|
+
keybindings?: BtwKeybindingOverrides;
|
|
16
|
+
model?: string;
|
|
17
|
+
thinkingLevel?: BtwThinkingLevel;
|
|
18
|
+
rememberThinkingLevelChanges?: boolean;
|
|
19
|
+
fullscreenCopyOnSelect?: boolean;
|
|
24
20
|
}
|
|
25
21
|
|
|
26
22
|
export type BtwSettingsLoadResult =
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
| { kind: "missing" }
|
|
24
|
+
| { kind: "invalid"; reason: string }
|
|
25
|
+
| { kind: "loaded"; settings: BtwSettings };
|
|
30
26
|
|
|
31
27
|
export interface BtwSettingsPatch {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
keybindings?: BtwKeybindingOverrides;
|
|
29
|
+
thinkingLevel?: BtwThinkingLevel;
|
|
30
|
+
rememberThinkingLevelChanges?: boolean;
|
|
31
|
+
fullscreenCopyOnSelect?: boolean;
|
|
36
32
|
}
|
|
37
33
|
|
|
38
34
|
export interface UpdateBtwSettingsOptions {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
/** Validate against the latest document inside the mutation queue, before applying the patch. */
|
|
36
|
+
validateCurrent?: (settings: BtwSettings) => void;
|
|
37
|
+
settingsPath?: string;
|
|
38
|
+
signal?: AbortSignal;
|
|
39
|
+
beforeRename?: (temporaryPath: string, settingsPath: string) => Promise<void>;
|
|
44
40
|
}
|
|
45
41
|
|
|
46
42
|
type SettingsDocument = Record<string, unknown>;
|
|
@@ -48,258 +44,246 @@ type SettingsDocument = Record<string, unknown>;
|
|
|
48
44
|
const mutationQueues = new Map<string, Promise<void>>();
|
|
49
45
|
|
|
50
46
|
export function btwSettingsPath(): string {
|
|
51
|
-
|
|
47
|
+
return join(getAgentDir(), BTW_SETTINGS_FILE);
|
|
52
48
|
}
|
|
53
49
|
|
|
54
50
|
export function normalizeBtwSettings(value: unknown): BtwSettings | undefined {
|
|
55
|
-
|
|
51
|
+
if (!isSettingsDocument(value)) return undefined;
|
|
56
52
|
|
|
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
|
-
|
|
53
|
+
const settings: BtwSettings = {};
|
|
54
|
+
if (Object.hasOwn(value, "keybindings")) {
|
|
55
|
+
const keys = value.keybindings;
|
|
56
|
+
if (!isSettingsDocument(keys)) return undefined;
|
|
57
|
+
settings.keybindings = {};
|
|
58
|
+
for (const action of BTW_SHORTCUT_ACTIONS) {
|
|
59
|
+
if (!Object.hasOwn(keys, action)) continue;
|
|
60
|
+
const key = normalizeBtwKey(keys[action]);
|
|
61
|
+
if (!key) return undefined;
|
|
62
|
+
settings.keybindings[action] = key;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (Object.hasOwn(value, "model")) {
|
|
66
|
+
const model = Reflect.get(value, "model");
|
|
67
|
+
if (typeof model !== "string" || !parseBtwModelReference(model)) return undefined;
|
|
68
|
+
settings.model = model;
|
|
69
|
+
}
|
|
70
|
+
if (Object.hasOwn(value, "thinkingLevel")) {
|
|
71
|
+
const thinkingLevel = Reflect.get(value, "thinkingLevel");
|
|
72
|
+
if (!isBtwThinkingLevel(thinkingLevel)) return undefined;
|
|
73
|
+
settings.thinkingLevel = thinkingLevel;
|
|
74
|
+
}
|
|
75
|
+
if (Object.hasOwn(value, "rememberThinkingLevelChanges")) {
|
|
76
|
+
const remember = Reflect.get(value, "rememberThinkingLevelChanges");
|
|
77
|
+
if (typeof remember !== "boolean") return undefined;
|
|
78
|
+
settings.rememberThinkingLevelChanges = remember;
|
|
79
|
+
}
|
|
80
|
+
if (Object.hasOwn(value, "fullscreenCopyOnSelect")) {
|
|
81
|
+
const copyOnSelect = Reflect.get(value, "fullscreenCopyOnSelect");
|
|
82
|
+
if (typeof copyOnSelect !== "boolean") return undefined;
|
|
83
|
+
settings.fullscreenCopyOnSelect = copyOnSelect;
|
|
84
|
+
}
|
|
85
|
+
return settings;
|
|
90
86
|
}
|
|
91
87
|
|
|
92
|
-
export function parseBtwModelReference(
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
if (separator <= 0 || separator === reference.length - 1) return undefined;
|
|
98
|
-
return { provider: reference.slice(0, separator), modelId: reference.slice(separator + 1) };
|
|
88
|
+
export function parseBtwModelReference(reference: string): { provider: string; modelId: string } | undefined {
|
|
89
|
+
if (/[\s\p{Cc}]/u.test(reference)) return undefined;
|
|
90
|
+
const separator = reference.indexOf("/");
|
|
91
|
+
if (separator <= 0 || separator === reference.length - 1) return undefined;
|
|
92
|
+
return { provider: reference.slice(0, separator), modelId: reference.slice(separator + 1) };
|
|
99
93
|
}
|
|
100
94
|
|
|
101
95
|
export function effectiveFullscreenCopyOnSelect(settings: BtwSettings): boolean {
|
|
102
|
-
|
|
96
|
+
return settings.fullscreenCopyOnSelect ?? DEFAULT_FULLSCREEN_COPY_ON_SELECT;
|
|
103
97
|
}
|
|
104
98
|
|
|
105
99
|
export function effectiveRememberThinkingLevelChanges(settings: BtwSettings): boolean {
|
|
106
|
-
|
|
100
|
+
return settings.rememberThinkingLevelChanges ?? DEFAULT_REMEMBER_THINKING_LEVEL_CHANGES;
|
|
107
101
|
}
|
|
108
102
|
|
|
109
|
-
export async function readBtwSettings(
|
|
110
|
-
|
|
111
|
-
)
|
|
112
|
-
await awaitBtwSettingsWrites(settingsPath);
|
|
113
|
-
return readBtwSettingsUncoordinated(settingsPath);
|
|
103
|
+
export async function readBtwSettings(settingsPath = btwSettingsPath()): Promise<BtwSettingsLoadResult> {
|
|
104
|
+
await awaitBtwSettingsWrites(settingsPath);
|
|
105
|
+
return readBtwSettingsUncoordinated(settingsPath);
|
|
114
106
|
}
|
|
115
107
|
|
|
116
108
|
export function updateBtwSettings(
|
|
117
|
-
|
|
118
|
-
|
|
109
|
+
patch: BtwSettingsPatch,
|
|
110
|
+
options: UpdateBtwSettingsOptions = {},
|
|
119
111
|
): Promise<BtwSettings> {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
112
|
+
const settingsPath = options.settingsPath ?? btwSettingsPath();
|
|
113
|
+
return enqueueMutation(settingsPath, async () => {
|
|
114
|
+
options.signal?.throwIfAborted();
|
|
115
|
+
const current = await readSettingsDocumentForUpdate(settingsPath);
|
|
116
|
+
options.signal?.throwIfAborted();
|
|
117
|
+
options.validateCurrent?.(normalizeBtwSettings(current) ?? {});
|
|
118
|
+
const updated = applyBtwSettingsPatch(current, patch);
|
|
119
|
+
const settings = normalizeBtwSettings(updated);
|
|
120
|
+
if (!settings) throw invalidSettingsError(settingsPath, "invalid settings shape");
|
|
121
|
+
await publishSettings(settingsPath, updated, options.signal, options.beforeRename);
|
|
122
|
+
return settings;
|
|
123
|
+
});
|
|
132
124
|
}
|
|
133
125
|
|
|
134
126
|
export async function awaitBtwSettingsWrites(settingsPath = btwSettingsPath()): Promise<void> {
|
|
135
|
-
|
|
127
|
+
await mutationQueues.get(settingsPath);
|
|
136
128
|
}
|
|
137
129
|
|
|
138
130
|
function enqueueMutation<T>(settingsPath: string, mutation: () => Promise<T>): Promise<T> {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
131
|
+
const previous = mutationQueues.get(settingsPath) ?? Promise.resolve();
|
|
132
|
+
const result = previous.then(mutation, mutation);
|
|
133
|
+
const settled = result.then(
|
|
134
|
+
() => undefined,
|
|
135
|
+
() => undefined,
|
|
136
|
+
);
|
|
137
|
+
mutationQueues.set(settingsPath, settled);
|
|
138
|
+
void settled.finally(() => {
|
|
139
|
+
if (mutationQueues.get(settingsPath) === settled) mutationQueues.delete(settingsPath);
|
|
140
|
+
});
|
|
141
|
+
return result;
|
|
150
142
|
}
|
|
151
143
|
|
|
152
144
|
async function readBtwSettingsUncoordinated(settingsPath: string): Promise<BtwSettingsLoadResult> {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
145
|
+
let contents: string;
|
|
146
|
+
try {
|
|
147
|
+
contents = await readSettingsContents(settingsPath);
|
|
148
|
+
} catch (error: unknown) {
|
|
149
|
+
if (isNodeError(error) && error.code === "ENOENT") return { kind: "missing" };
|
|
150
|
+
return { kind: "invalid", reason: `${settingsPath}: ${formatError(error)}` };
|
|
151
|
+
}
|
|
160
152
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
153
|
+
try {
|
|
154
|
+
const settings = normalizeBtwSettings(JSON.parse(contents) as unknown);
|
|
155
|
+
return settings
|
|
156
|
+
? { kind: "loaded", settings }
|
|
157
|
+
: { kind: "invalid", reason: `${settingsPath}: invalid settings shape` };
|
|
158
|
+
} catch {
|
|
159
|
+
return { kind: "invalid", reason: `${settingsPath}: invalid JSON` };
|
|
160
|
+
}
|
|
169
161
|
}
|
|
170
162
|
|
|
171
163
|
async function readSettingsDocumentForUpdate(settingsPath: string): Promise<SettingsDocument> {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
164
|
+
let contents: string;
|
|
165
|
+
try {
|
|
166
|
+
contents = await readSettingsContents(settingsPath);
|
|
167
|
+
} catch (error: unknown) {
|
|
168
|
+
if (isNodeError(error) && error.code === "ENOENT") return {};
|
|
169
|
+
throw invalidSettingsError(settingsPath, formatError(error));
|
|
170
|
+
}
|
|
179
171
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
172
|
+
let parsed: unknown;
|
|
173
|
+
try {
|
|
174
|
+
parsed = JSON.parse(contents) as unknown;
|
|
175
|
+
} catch {
|
|
176
|
+
throw invalidSettingsError(settingsPath, "invalid JSON");
|
|
177
|
+
}
|
|
178
|
+
if (!isSettingsDocument(parsed) || !normalizeBtwSettings(parsed)) {
|
|
179
|
+
throw invalidSettingsError(settingsPath, "invalid settings shape");
|
|
180
|
+
}
|
|
181
|
+
return parsed;
|
|
190
182
|
}
|
|
191
183
|
|
|
192
184
|
async function readSettingsContents(settingsPath: string): Promise<string> {
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
185
|
+
const flags = constants.O_RDONLY | (constants.O_NONBLOCK ?? 0);
|
|
186
|
+
const handle = await open(settingsPath, flags);
|
|
187
|
+
try {
|
|
188
|
+
const descriptorStats = await handle.stat();
|
|
189
|
+
if (!descriptorStats.isFile()) throw new Error("settings path is not a regular file");
|
|
190
|
+
if (descriptorStats.size > MAX_SETTINGS_BYTES) {
|
|
191
|
+
throw new Error(`settings file exceeds ${MAX_SETTINGS_BYTES} bytes`);
|
|
192
|
+
}
|
|
201
193
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
await handle.close();
|
|
221
|
-
}
|
|
194
|
+
const buffer = Buffer.alloc(MAX_SETTINGS_BYTES + 1);
|
|
195
|
+
let offset = 0;
|
|
196
|
+
while (offset < buffer.byteLength) {
|
|
197
|
+
const { bytesRead } = await handle.read(buffer, offset, buffer.byteLength - offset, offset);
|
|
198
|
+
if (bytesRead === 0) break;
|
|
199
|
+
offset += bytesRead;
|
|
200
|
+
}
|
|
201
|
+
if (offset > MAX_SETTINGS_BYTES) {
|
|
202
|
+
throw new Error(`settings file exceeds ${MAX_SETTINGS_BYTES} bytes`);
|
|
203
|
+
}
|
|
204
|
+
try {
|
|
205
|
+
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(buffer.subarray(0, offset));
|
|
206
|
+
} catch {
|
|
207
|
+
throw new Error("settings file is not valid UTF-8");
|
|
208
|
+
}
|
|
209
|
+
} finally {
|
|
210
|
+
await handle.close();
|
|
211
|
+
}
|
|
222
212
|
}
|
|
223
213
|
|
|
224
214
|
async function publishSettings(
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
215
|
+
settingsPath: string,
|
|
216
|
+
document: SettingsDocument,
|
|
217
|
+
signal?: AbortSignal,
|
|
218
|
+
beforeRename?: (temporaryPath: string, settingsPath: string) => Promise<void>,
|
|
229
219
|
): Promise<void> {
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
await rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
254
|
-
throw error;
|
|
255
|
-
}
|
|
220
|
+
signal?.throwIfAborted();
|
|
221
|
+
const contents = `${JSON.stringify(document, null, 2)}\n`;
|
|
222
|
+
if (Buffer.byteLength(contents, "utf8") > MAX_SETTINGS_BYTES) {
|
|
223
|
+
throw new Error(`settings document exceeds ${MAX_SETTINGS_BYTES} bytes`);
|
|
224
|
+
}
|
|
225
|
+
const directory = dirname(settingsPath);
|
|
226
|
+
await mkdir(directory, { recursive: true });
|
|
227
|
+
signal?.throwIfAborted();
|
|
228
|
+
const temporaryPath = join(directory, `.${basename(settingsPath)}.${process.pid}.${randomUUID()}.tmp`);
|
|
229
|
+
try {
|
|
230
|
+
await writeFile(temporaryPath, contents, {
|
|
231
|
+
encoding: "utf8",
|
|
232
|
+
flag: "wx",
|
|
233
|
+
mode: 0o600,
|
|
234
|
+
signal,
|
|
235
|
+
});
|
|
236
|
+
await beforeRename?.(temporaryPath, settingsPath);
|
|
237
|
+
signal?.throwIfAborted();
|
|
238
|
+
await rename(temporaryPath, settingsPath);
|
|
239
|
+
} catch (error) {
|
|
240
|
+
await rm(temporaryPath, { force: true }).catch(() => undefined);
|
|
241
|
+
throw error;
|
|
242
|
+
}
|
|
256
243
|
}
|
|
257
244
|
|
|
258
|
-
function applyBtwSettingsPatch(
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
)
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
else updated.fullscreenCopyOnSelect = patch.fullscreenCopyOnSelect;
|
|
283
|
-
}
|
|
284
|
-
return updated;
|
|
245
|
+
function applyBtwSettingsPatch(current: SettingsDocument, patch: BtwSettingsPatch): SettingsDocument {
|
|
246
|
+
const updated: SettingsDocument = { ...current };
|
|
247
|
+
if (patch.keybindings) {
|
|
248
|
+
const keys = isSettingsDocument(current.keybindings) ? { ...current.keybindings } : {};
|
|
249
|
+
for (const action of BTW_SHORTCUT_ACTIONS) {
|
|
250
|
+
if (!Object.hasOwn(patch.keybindings, action)) continue;
|
|
251
|
+
if (patch.keybindings[action] === undefined) delete keys[action];
|
|
252
|
+
else keys[action] = patch.keybindings[action];
|
|
253
|
+
}
|
|
254
|
+
if (Object.keys(keys).length) updated.keybindings = keys;
|
|
255
|
+
else delete updated.keybindings;
|
|
256
|
+
}
|
|
257
|
+
if (Object.hasOwn(patch, "thinkingLevel")) {
|
|
258
|
+
if (patch.thinkingLevel === undefined) delete updated.thinkingLevel;
|
|
259
|
+
else updated.thinkingLevel = patch.thinkingLevel;
|
|
260
|
+
}
|
|
261
|
+
if (Object.hasOwn(patch, "rememberThinkingLevelChanges")) {
|
|
262
|
+
updated.rememberThinkingLevelChanges = patch.rememberThinkingLevelChanges;
|
|
263
|
+
}
|
|
264
|
+
if (Object.hasOwn(patch, "fullscreenCopyOnSelect")) {
|
|
265
|
+
if (patch.fullscreenCopyOnSelect === undefined) delete updated.fullscreenCopyOnSelect;
|
|
266
|
+
else updated.fullscreenCopyOnSelect = patch.fullscreenCopyOnSelect;
|
|
267
|
+
}
|
|
268
|
+
return updated;
|
|
285
269
|
}
|
|
286
270
|
|
|
287
271
|
function isSettingsDocument(value: unknown): value is SettingsDocument {
|
|
288
|
-
|
|
272
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
289
273
|
}
|
|
290
274
|
|
|
291
275
|
function isBtwThinkingLevel(value: unknown): value is BtwThinkingLevel {
|
|
292
|
-
|
|
276
|
+
return BTW_THINKING_LEVELS.includes(value as BtwThinkingLevel);
|
|
293
277
|
}
|
|
294
278
|
|
|
295
279
|
function invalidSettingsError(settingsPath: string, reason: string): Error {
|
|
296
|
-
|
|
280
|
+
return new Error(`pi-btw settings at ${settingsPath} are invalid: ${reason}`);
|
|
297
281
|
}
|
|
298
282
|
|
|
299
283
|
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
|
|
300
|
-
|
|
284
|
+
return error instanceof Error && "code" in error;
|
|
301
285
|
}
|
|
302
286
|
|
|
303
287
|
function formatError(error: unknown): string {
|
|
304
|
-
|
|
288
|
+
return error instanceof Error ? error.message : String(error);
|
|
305
289
|
}
|