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