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