@narumitw/pi-usage 0.60.6 → 0.60.8

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
@@ -10,373 +10,344 @@ export const USAGE_SETTINGS_FILE = "pi-usage.json";
10
10
  export const MAX_USAGE_SETTINGS_BYTES = 64 * 1024;
11
11
 
12
12
  export interface UsageSettings {
13
- codexFastMode: boolean;
14
- codexStatusResetCountdown: boolean;
15
- selectedTargets: Record<string, string>;
13
+ codexFastMode: boolean;
14
+ codexStatusResetCountdown: boolean;
15
+ selectedTargets: Record<string, string>;
16
16
  }
17
17
 
18
18
  export const DEFAULT_USAGE_SETTINGS: Readonly<UsageSettings> = Object.freeze({
19
- codexFastMode: false,
20
- codexStatusResetCountdown: true,
21
- selectedTargets: Object.freeze({}),
19
+ codexFastMode: false,
20
+ codexStatusResetCountdown: true,
21
+ selectedTargets: Object.freeze({}),
22
22
  });
23
23
 
24
24
  export interface UsageSettingsState {
25
- kind: "missing" | "loaded" | "invalid";
26
- path: string;
27
- settings: UsageSettings;
28
- document?: Record<string, unknown>;
29
- issue?: string;
25
+ kind: "missing" | "loaded" | "invalid";
26
+ path: string;
27
+ settings: UsageSettings;
28
+ document?: Record<string, unknown>;
29
+ issue?: string;
30
30
  }
31
31
 
32
32
  export type UsageTargetPublicationCheck = () => Promise<void>;
33
33
 
34
34
  export interface UsageSettingsRuntime {
35
- get(): Readonly<UsageSettingsState>;
36
- reload(signal?: AbortSignal): Promise<Readonly<UsageSettingsState>>;
37
- update(
38
- patch: Partial<UsageSettings>,
39
- signal?: AbortSignal,
40
- ): Promise<Readonly<UsageSettingsState>>;
41
- updateSelectedTarget(
42
- providerId: string,
43
- targetId: string,
44
- signal?: AbortSignal,
45
- checkPublishedSelection?: UsageTargetPublicationCheck,
46
- ): Promise<Readonly<UsageSettingsState>>;
47
- flush(): Promise<void>;
35
+ get(): Readonly<UsageSettingsState>;
36
+ reload(signal?: AbortSignal): Promise<Readonly<UsageSettingsState>>;
37
+ update(patch: Partial<UsageSettings>, signal?: AbortSignal): Promise<Readonly<UsageSettingsState>>;
38
+ updateSelectedTarget(
39
+ providerId: string,
40
+ targetId: string,
41
+ signal?: AbortSignal,
42
+ checkPublishedSelection?: UsageTargetPublicationCheck,
43
+ ): Promise<Readonly<UsageSettingsState>>;
44
+ flush(): Promise<void>;
48
45
  }
49
46
 
50
47
  interface UsageSettingsFileOperations {
51
- rename: typeof rename;
52
- writeFile: typeof writeFile;
48
+ rename: typeof rename;
49
+ writeFile: typeof writeFile;
53
50
  }
54
51
 
55
52
  interface UsageSettingsRuntimeOptions {
56
- operations?: Partial<UsageSettingsFileOperations>;
57
- path?: string;
53
+ operations?: Partial<UsageSettingsFileOperations>;
54
+ path?: string;
58
55
  }
59
56
 
60
57
  export function usageSettingsPath(): string {
61
- return join(getAgentDir(), USAGE_SETTINGS_FILE);
58
+ return join(getAgentDir(), USAGE_SETTINGS_FILE);
62
59
  }
63
60
 
64
61
  export function normalizeUsageSettings(value: unknown): UsageSettings | undefined {
65
- if (!isRecord(value)) return undefined;
66
- if (Object.hasOwn(value, "codexFastMode") && typeof value.codexFastMode !== "boolean") {
67
- return undefined;
68
- }
69
- if (
70
- Object.hasOwn(value, "codexStatusResetCountdown") &&
71
- typeof value.codexStatusResetCountdown !== "boolean"
72
- ) {
73
- return undefined;
74
- }
75
- if (
76
- Object.hasOwn(value, "fireworksAccountId") &&
77
- !isFireworksAccountId(value.fireworksAccountId)
78
- ) {
79
- return undefined;
80
- }
81
- const selectedTargets = normalizeSelectedTargets(value.selectedTargets);
82
- if (Object.hasOwn(value, "selectedTargets") && !selectedTargets) return undefined;
83
- const effectiveTargets = { ...(selectedTargets ?? {}) };
84
- if (!effectiveTargets.fireworks && isFireworksAccountId(value.fireworksAccountId)) {
85
- effectiveTargets.fireworks = value.fireworksAccountId;
86
- }
87
- return {
88
- codexFastMode:
89
- typeof value.codexFastMode === "boolean"
90
- ? value.codexFastMode
91
- : DEFAULT_USAGE_SETTINGS.codexFastMode,
92
- codexStatusResetCountdown:
93
- typeof value.codexStatusResetCountdown === "boolean"
94
- ? value.codexStatusResetCountdown
95
- : DEFAULT_USAGE_SETTINGS.codexStatusResetCountdown,
96
- selectedTargets: effectiveTargets,
97
- };
62
+ if (!isRecord(value)) return undefined;
63
+ if (Object.hasOwn(value, "codexFastMode") && typeof value.codexFastMode !== "boolean") {
64
+ return undefined;
65
+ }
66
+ if (Object.hasOwn(value, "codexStatusResetCountdown") && typeof value.codexStatusResetCountdown !== "boolean") {
67
+ return undefined;
68
+ }
69
+ if (Object.hasOwn(value, "fireworksAccountId") && !isFireworksAccountId(value.fireworksAccountId)) {
70
+ return undefined;
71
+ }
72
+ const selectedTargets = normalizeSelectedTargets(value.selectedTargets);
73
+ if (Object.hasOwn(value, "selectedTargets") && !selectedTargets) return undefined;
74
+ const effectiveTargets = { ...(selectedTargets ?? {}) };
75
+ if (!effectiveTargets.fireworks && isFireworksAccountId(value.fireworksAccountId)) {
76
+ effectiveTargets.fireworks = value.fireworksAccountId;
77
+ }
78
+ return {
79
+ codexFastMode:
80
+ typeof value.codexFastMode === "boolean" ? value.codexFastMode : DEFAULT_USAGE_SETTINGS.codexFastMode,
81
+ codexStatusResetCountdown:
82
+ typeof value.codexStatusResetCountdown === "boolean"
83
+ ? value.codexStatusResetCountdown
84
+ : DEFAULT_USAGE_SETTINGS.codexStatusResetCountdown,
85
+ selectedTargets: effectiveTargets,
86
+ };
98
87
  }
99
88
 
100
- export async function loadUsageSettings(
101
- path = usageSettingsPath(),
102
- signal?: AbortSignal,
103
- ): Promise<UsageSettingsState> {
104
- throwIfAborted(signal);
105
- try {
106
- const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
107
- let text: string;
108
- try {
109
- const stats = await handle.stat();
110
- throwIfAborted(signal);
111
- if (!stats.isFile()) throw new Error("settings path is not a regular file");
112
- if (stats.size > MAX_USAGE_SETTINGS_BYTES) {
113
- throw new Error("settings file exceeds 64 KiB");
114
- }
115
- text = await handle.readFile("utf8");
116
- } finally {
117
- await handle.close();
118
- }
119
- throwIfAborted(signal);
120
- const document = JSON.parse(text) as unknown;
121
- const settings = normalizeUsageSettings(document);
122
- if (!settings || !isRecord(document)) throw new Error("invalid settings shape");
123
- return { kind: "loaded", path, settings, document };
124
- } catch (error) {
125
- if (signal?.aborted) throw error;
126
- if (isNodeError(error) && error.code === "ENOENT") {
127
- return {
128
- kind: "missing",
129
- path,
130
- settings: { ...DEFAULT_USAGE_SETTINGS },
131
- document: {},
132
- };
133
- }
134
- return {
135
- kind: "invalid",
136
- path,
137
- settings: { ...DEFAULT_USAGE_SETTINGS },
138
- issue:
139
- isNodeError(error) && error.code === "ELOOP"
140
- ? "symbolic links are not accepted"
141
- : error instanceof Error
142
- ? error.message
143
- : String(error),
144
- };
145
- }
89
+ export async function loadUsageSettings(path = usageSettingsPath(), signal?: AbortSignal): Promise<UsageSettingsState> {
90
+ throwIfAborted(signal);
91
+ try {
92
+ const handle = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW);
93
+ let text: string;
94
+ try {
95
+ const stats = await handle.stat();
96
+ throwIfAborted(signal);
97
+ if (!stats.isFile()) throw new Error("settings path is not a regular file");
98
+ if (stats.size > MAX_USAGE_SETTINGS_BYTES) {
99
+ throw new Error("settings file exceeds 64 KiB");
100
+ }
101
+ text = await handle.readFile("utf8");
102
+ } finally {
103
+ await handle.close();
104
+ }
105
+ throwIfAborted(signal);
106
+ const document = JSON.parse(text) as unknown;
107
+ const settings = normalizeUsageSettings(document);
108
+ if (!settings || !isRecord(document)) throw new Error("invalid settings shape");
109
+ return { kind: "loaded", path, settings, document };
110
+ } catch (error) {
111
+ if (signal?.aborted) throw error;
112
+ if (isNodeError(error) && error.code === "ENOENT") {
113
+ return {
114
+ kind: "missing",
115
+ path,
116
+ settings: { ...DEFAULT_USAGE_SETTINGS },
117
+ document: {},
118
+ };
119
+ }
120
+ return {
121
+ kind: "invalid",
122
+ path,
123
+ settings: { ...DEFAULT_USAGE_SETTINGS },
124
+ issue:
125
+ isNodeError(error) && error.code === "ELOOP"
126
+ ? "symbolic links are not accepted"
127
+ : error instanceof Error
128
+ ? error.message
129
+ : String(error),
130
+ };
131
+ }
146
132
  }
147
133
 
148
- export function createUsageSettingsRuntime(
149
- options: UsageSettingsRuntimeOptions | string = {},
150
- ): UsageSettingsRuntime {
151
- const path = typeof options === "string" ? options : (options.path ?? usageSettingsPath());
152
- const operations: UsageSettingsFileOperations = {
153
- rename,
154
- writeFile,
155
- ...(typeof options === "string" ? undefined : options.operations),
156
- };
157
- let state: UsageSettingsState = {
158
- kind: "missing",
159
- path,
160
- settings: { ...DEFAULT_USAGE_SETTINGS },
161
- document: {},
162
- };
163
- let queue = Promise.resolve();
164
- const enqueue = <T>(operation: () => Promise<T>): Promise<T> => {
165
- const result = queue.then(operation, operation);
166
- queue = result.then(
167
- () => undefined,
168
- () => undefined,
169
- );
170
- return result;
171
- };
172
- return {
173
- get: () => structuredClone(state),
174
- reload: (signal) =>
175
- enqueue(async () => {
176
- const loaded = await loadUsageSettings(path, signal);
177
- state = loaded;
178
- return structuredClone(state);
179
- }),
180
- update: (patch, signal) =>
181
- enqueue(async () => {
182
- const saved = await saveUsageSettingsPatch(path, patch, operations, signal);
183
- state = saved;
184
- return structuredClone(state);
185
- }),
186
- updateSelectedTarget: (providerId, targetId, signal, checkPublishedSelection) =>
187
- enqueue(async () => {
188
- const transaction = await saveUsageTargetSelection(
189
- path,
190
- providerId,
191
- targetId,
192
- operations,
193
- signal,
194
- );
195
- try {
196
- await checkPublishedSelection?.();
197
- throwIfAborted(signal);
198
- } catch (error) {
199
- try {
200
- await restoreUsageSettingsState(
201
- path,
202
- transaction.saved,
203
- transaction.previous,
204
- operations,
205
- );
206
- state = transaction.previous;
207
- } catch (rollbackError) {
208
- state = await loadUsageSettings(path);
209
- throw new AggregateError(
210
- [error, rollbackError],
211
- "Target selection changed after publication and pi-usage.json rollback failed",
212
- );
213
- }
214
- throw error;
215
- }
216
- state = transaction.saved;
217
- return structuredClone(state);
218
- }),
219
- flush: () => queue,
220
- };
134
+ export function createUsageSettingsRuntime(options: UsageSettingsRuntimeOptions | string = {}): UsageSettingsRuntime {
135
+ const path = typeof options === "string" ? options : (options.path ?? usageSettingsPath());
136
+ const operations: UsageSettingsFileOperations = {
137
+ rename,
138
+ writeFile,
139
+ ...(typeof options === "string" ? undefined : options.operations),
140
+ };
141
+ let state: UsageSettingsState = {
142
+ kind: "missing",
143
+ path,
144
+ settings: { ...DEFAULT_USAGE_SETTINGS },
145
+ document: {},
146
+ };
147
+ let queue = Promise.resolve();
148
+ const enqueue = <T>(operation: () => Promise<T>): Promise<T> => {
149
+ const result = queue.then(operation, operation);
150
+ queue = result.then(
151
+ () => undefined,
152
+ () => undefined,
153
+ );
154
+ return result;
155
+ };
156
+ return {
157
+ get: () => structuredClone(state),
158
+ reload: (signal) =>
159
+ enqueue(async () => {
160
+ const loaded = await loadUsageSettings(path, signal);
161
+ state = loaded;
162
+ return structuredClone(state);
163
+ }),
164
+ update: (patch, signal) =>
165
+ enqueue(async () => {
166
+ const saved = await saveUsageSettingsPatch(path, patch, operations, signal);
167
+ state = saved;
168
+ return structuredClone(state);
169
+ }),
170
+ updateSelectedTarget: (providerId, targetId, signal, checkPublishedSelection) =>
171
+ enqueue(async () => {
172
+ const transaction = await saveUsageTargetSelection(path, providerId, targetId, operations, signal);
173
+ try {
174
+ await checkPublishedSelection?.();
175
+ throwIfAborted(signal);
176
+ } catch (error) {
177
+ try {
178
+ await restoreUsageSettingsState(path, transaction.saved, transaction.previous, operations);
179
+ state = transaction.previous;
180
+ } catch (rollbackError) {
181
+ state = await loadUsageSettings(path);
182
+ throw new AggregateError(
183
+ [error, rollbackError],
184
+ "Target selection changed after publication and pi-usage.json rollback failed",
185
+ );
186
+ }
187
+ throw error;
188
+ }
189
+ state = transaction.saved;
190
+ return structuredClone(state);
191
+ }),
192
+ flush: () => queue,
193
+ };
221
194
  }
222
195
 
223
196
  async function saveUsageSettingsPatch(
224
- path: string,
225
- patch: Partial<UsageSettings>,
226
- operations: UsageSettingsFileOperations,
227
- signal?: AbortSignal,
197
+ path: string,
198
+ patch: Partial<UsageSettings>,
199
+ operations: UsageSettingsFileOperations,
200
+ signal?: AbortSignal,
228
201
  ): Promise<UsageSettingsState> {
229
- return saveUsageSettingsDocument(
230
- path,
231
- (document) => {
232
- for (const [key, value] of Object.entries(patch)) {
233
- if (value === undefined) delete document[key];
234
- else document[key] = value;
235
- }
236
- },
237
- operations,
238
- signal,
239
- );
202
+ return saveUsageSettingsDocument(
203
+ path,
204
+ (document) => {
205
+ for (const [key, value] of Object.entries(patch)) {
206
+ if (value === undefined) delete document[key];
207
+ else document[key] = value;
208
+ }
209
+ },
210
+ operations,
211
+ signal,
212
+ );
240
213
  }
241
214
 
242
215
  async function saveUsageTargetSelection(
243
- path: string,
244
- providerId: string,
245
- targetId: string,
246
- operations: UsageSettingsFileOperations,
247
- signal?: AbortSignal,
216
+ path: string,
217
+ providerId: string,
218
+ targetId: string,
219
+ operations: UsageSettingsFileOperations,
220
+ signal?: AbortSignal,
248
221
  ): Promise<{ saved: UsageSettingsState; previous: UsageSettingsState }> {
249
- if (!isProviderId(providerId) || !isBoundedTargetId(targetId)) {
250
- throw new Error("Refusing to save an invalid usage target selection");
251
- }
252
- const previous = await loadUsageSettings(path, signal);
253
- const saved = await saveUsageSettingsDocument(
254
- path,
255
- (document) => {
256
- document.selectedTargets = {
257
- ...(normalizeSelectedTargets(document.selectedTargets) ?? {}),
258
- [providerId]: targetId,
259
- };
260
- if (providerId === "fireworks") delete document.fireworksAccountId;
261
- },
262
- operations,
263
- signal,
264
- previous,
265
- );
266
- return { saved, previous };
222
+ if (!isProviderId(providerId) || !isBoundedTargetId(targetId)) {
223
+ throw new Error("Refusing to save an invalid usage target selection");
224
+ }
225
+ const previous = await loadUsageSettings(path, signal);
226
+ const saved = await saveUsageSettingsDocument(
227
+ path,
228
+ (document) => {
229
+ document.selectedTargets = {
230
+ ...(normalizeSelectedTargets(document.selectedTargets) ?? {}),
231
+ [providerId]: targetId,
232
+ };
233
+ if (providerId === "fireworks") delete document.fireworksAccountId;
234
+ },
235
+ operations,
236
+ signal,
237
+ previous,
238
+ );
239
+ return { saved, previous };
267
240
  }
268
241
 
269
242
  async function saveUsageSettingsDocument(
270
- path: string,
271
- mutate: (document: Record<string, unknown>) => void,
272
- operations: UsageSettingsFileOperations,
273
- signal?: AbortSignal,
274
- expected?: UsageSettingsState,
243
+ path: string,
244
+ mutate: (document: Record<string, unknown>) => void,
245
+ operations: UsageSettingsFileOperations,
246
+ signal?: AbortSignal,
247
+ expected?: UsageSettingsState,
275
248
  ): Promise<UsageSettingsState> {
276
- const latest = await loadUsageSettings(path, signal);
277
- if (latest.kind === "invalid") {
278
- throw new Error("Cannot overwrite an invalid pi-usage.json; repair it and reload first");
279
- }
280
- if (expected && !sameUsageSettingsDocument(latest, expected)) {
281
- throw new Error("pi-usage.json changed while saving; retry the action");
282
- }
283
- const document = { ...latest.document };
284
- mutate(document);
285
- const settings = normalizeUsageSettings(document);
286
- if (!settings) throw new Error("Refusing to save invalid pi-usage settings");
287
- const directory = dirname(path);
288
- const temporaryPath = join(directory, `.${basename(path)}.${randomUUID()}.tmp`);
289
- await mkdir(directory, { recursive: true, mode: 0o700 });
290
- throwIfAborted(signal);
291
- try {
292
- await operations.writeFile(temporaryPath, `${JSON.stringify(document, null, 2)}\n`, {
293
- encoding: "utf8",
294
- flag: "wx",
295
- mode: 0o600,
296
- });
297
- if (process.platform !== "win32") await chmodPrivate(temporaryPath);
298
- throwIfAborted(signal);
299
- const current = await loadUsageSettings(path, signal);
300
- if (
301
- current.kind === "invalid" ||
302
- current.kind !== latest.kind ||
303
- JSON.stringify(current.document) !== JSON.stringify(latest.document)
304
- ) {
305
- throw new Error("pi-usage.json changed while saving; retry the action");
306
- }
307
- throwIfAborted(signal);
308
- await operations.rename(temporaryPath, path);
309
- } finally {
310
- await rm(temporaryPath, { force: true }).catch(() => undefined);
311
- }
312
- return { kind: "loaded", path, settings, document };
249
+ const latest = await loadUsageSettings(path, signal);
250
+ if (latest.kind === "invalid") {
251
+ throw new Error("Cannot overwrite an invalid pi-usage.json; repair it and reload first");
252
+ }
253
+ if (expected && !sameUsageSettingsDocument(latest, expected)) {
254
+ throw new Error("pi-usage.json changed while saving; retry the action");
255
+ }
256
+ const document = { ...latest.document };
257
+ mutate(document);
258
+ const settings = normalizeUsageSettings(document);
259
+ if (!settings) throw new Error("Refusing to save invalid pi-usage settings");
260
+ const directory = dirname(path);
261
+ const temporaryPath = join(directory, `.${basename(path)}.${randomUUID()}.tmp`);
262
+ await mkdir(directory, { recursive: true, mode: 0o700 });
263
+ throwIfAborted(signal);
264
+ try {
265
+ await operations.writeFile(temporaryPath, `${JSON.stringify(document, null, 2)}\n`, {
266
+ encoding: "utf8",
267
+ flag: "wx",
268
+ mode: 0o600,
269
+ });
270
+ if (process.platform !== "win32") await chmodPrivate(temporaryPath);
271
+ throwIfAborted(signal);
272
+ const current = await loadUsageSettings(path, signal);
273
+ if (
274
+ current.kind === "invalid" ||
275
+ current.kind !== latest.kind ||
276
+ JSON.stringify(current.document) !== JSON.stringify(latest.document)
277
+ ) {
278
+ throw new Error("pi-usage.json changed while saving; retry the action");
279
+ }
280
+ throwIfAborted(signal);
281
+ await operations.rename(temporaryPath, path);
282
+ } finally {
283
+ await rm(temporaryPath, { force: true }).catch(() => undefined);
284
+ }
285
+ return { kind: "loaded", path, settings, document };
313
286
  }
314
287
 
315
288
  async function restoreUsageSettingsState(
316
- path: string,
317
- published: UsageSettingsState,
318
- previous: UsageSettingsState,
319
- operations: UsageSettingsFileOperations,
289
+ path: string,
290
+ published: UsageSettingsState,
291
+ previous: UsageSettingsState,
292
+ operations: UsageSettingsFileOperations,
320
293
  ): Promise<void> {
321
- if (previous.kind === "missing") {
322
- const current = await loadUsageSettings(path);
323
- if (!sameUsageSettingsDocument(current, published)) {
324
- throw new Error("pi-usage.json changed before target selection rollback");
325
- }
326
- await rm(path);
327
- return;
328
- }
329
- if (previous.kind !== "loaded" || !previous.document) {
330
- throw new Error("Cannot restore invalid prior pi-usage.json settings");
331
- }
332
- await saveUsageSettingsDocument(
333
- path,
334
- (document) => {
335
- for (const key of Object.keys(document)) delete document[key];
336
- Object.assign(document, previous.document);
337
- },
338
- operations,
339
- undefined,
340
- published,
341
- );
294
+ if (previous.kind === "missing") {
295
+ const current = await loadUsageSettings(path);
296
+ if (!sameUsageSettingsDocument(current, published)) {
297
+ throw new Error("pi-usage.json changed before target selection rollback");
298
+ }
299
+ await rm(path);
300
+ return;
301
+ }
302
+ if (previous.kind !== "loaded" || !previous.document) {
303
+ throw new Error("Cannot restore invalid prior pi-usage.json settings");
304
+ }
305
+ await saveUsageSettingsDocument(
306
+ path,
307
+ (document) => {
308
+ for (const key of Object.keys(document)) delete document[key];
309
+ Object.assign(document, previous.document);
310
+ },
311
+ operations,
312
+ undefined,
313
+ published,
314
+ );
342
315
  }
343
316
 
344
317
  function sameUsageSettingsDocument(
345
- left: Pick<UsageSettingsState, "kind" | "document">,
346
- right: Pick<UsageSettingsState, "kind" | "document">,
318
+ left: Pick<UsageSettingsState, "kind" | "document">,
319
+ right: Pick<UsageSettingsState, "kind" | "document">,
347
320
  ): boolean {
348
- return (
349
- left.kind === right.kind && JSON.stringify(left.document) === JSON.stringify(right.document)
350
- );
321
+ return left.kind === right.kind && JSON.stringify(left.document) === JSON.stringify(right.document);
351
322
  }
352
323
 
353
324
  async function chmodPrivate(path: string): Promise<void> {
354
- await chmod(path, 0o600);
325
+ await chmod(path, 0o600);
355
326
  }
356
327
 
357
328
  function throwIfAborted(signal?: AbortSignal): void {
358
- if (signal?.aborted) throw new DOMException("Settings operation aborted", "AbortError");
329
+ if (signal?.aborted) throw new DOMException("Settings operation aborted", "AbortError");
359
330
  }
360
331
 
361
332
  function isRecord(value: unknown): value is Record<string, unknown> {
362
- return typeof value === "object" && value !== null && !Array.isArray(value);
333
+ return typeof value === "object" && value !== null && !Array.isArray(value);
363
334
  }
364
335
 
365
336
  function isNodeError(error: unknown): error is NodeJS.ErrnoException {
366
- return error instanceof Error && "code" in error;
337
+ return error instanceof Error && "code" in error;
367
338
  }
368
339
 
369
340
  function normalizeSelectedTargets(value: unknown): Record<string, string> | undefined {
370
- if (value === undefined) return {};
371
- if (!isRecord(value)) return undefined;
372
- const targets: Record<string, string> = {};
373
- for (const [providerId, targetId] of Object.entries(value)) {
374
- if (!isProviderId(providerId) || !isBoundedTargetId(targetId)) return undefined;
375
- targets[providerId] = targetId;
376
- }
377
- return targets;
341
+ if (value === undefined) return {};
342
+ if (!isRecord(value)) return undefined;
343
+ const targets: Record<string, string> = {};
344
+ for (const [providerId, targetId] of Object.entries(value)) {
345
+ if (!isProviderId(providerId) || !isBoundedTargetId(targetId)) return undefined;
346
+ targets[providerId] = targetId;
347
+ }
348
+ return targets;
378
349
  }
379
350
 
380
351
  function isProviderId(value: string): boolean {
381
- return /^[A-Za-z0-9][A-Za-z0-9._~-]{0,127}$/u.test(value);
352
+ return /^[A-Za-z0-9][A-Za-z0-9._~-]{0,127}$/u.test(value);
382
353
  }