@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/dist/index.ts +104 -363
- package/dist/index.ts.map +2 -2
- package/docs/providers.md +2 -1
- package/package.json +73 -74
- package/src/codex-fast-runtime.ts +148 -184
- package/src/codex-fast.ts +74 -83
- package/src/codex-resets.ts +249 -278
- package/src/core.ts +204 -206
- package/src/format.ts +519 -574
- package/src/index.ts +77 -77
- package/src/oauth-credential-source.ts +57 -65
- package/src/providers/baseten.ts +32 -36
- package/src/providers/codex.ts +109 -120
- package/src/providers/deepseek.ts +59 -64
- package/src/providers/fireworks.ts +219 -238
- package/src/providers/github-copilot.ts +89 -95
- package/src/providers/kimi-coding.ts +202 -213
- package/src/providers/minimax.ts +214 -219
- package/src/providers/moonshot.ts +41 -46
- package/src/providers/opencode-zen.ts +56 -59
- package/src/providers/openrouter.ts +50 -53
- package/src/providers/vercel-ai-gateway.ts +33 -33
- package/src/providers/xai.ts +141 -148
- package/src/providers/zai-errors.ts +75 -75
- package/src/providers/zai.ts +173 -189
- package/src/query.ts +887 -967
- package/src/settings.ts +275 -304
- package/src/types.ts +145 -145
- package/src/usage-helpers.ts +18 -18
- package/src/usage-settings-ui.ts +118 -145
- package/src/usage-targets.ts +98 -114
- package/src/usage.ts +1264 -1426
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
codexFastMode: false,
|
|
20
|
+
codexStatusResetCountdown: true,
|
|
21
|
+
selectedTargets: Object.freeze({}),
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
export interface UsageSettingsState {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
52
|
-
|
|
48
|
+
rename: typeof rename;
|
|
49
|
+
writeFile: typeof writeFile;
|
|
53
50
|
}
|
|
54
51
|
|
|
55
52
|
interface UsageSettingsRuntimeOptions {
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
operations?: Partial<UsageSettingsFileOperations>;
|
|
54
|
+
path?: string;
|
|
58
55
|
}
|
|
59
56
|
|
|
60
57
|
export function usageSettingsPath(): string {
|
|
61
|
-
|
|
58
|
+
return join(getAgentDir(), USAGE_SETTINGS_FILE);
|
|
62
59
|
}
|
|
63
60
|
|
|
64
61
|
export function normalizeUsageSettings(value: unknown): UsageSettings | undefined {
|
|
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
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
197
|
+
path: string,
|
|
198
|
+
patch: Partial<UsageSettings>,
|
|
199
|
+
operations: UsageSettingsFileOperations,
|
|
200
|
+
signal?: AbortSignal,
|
|
228
201
|
): Promise<UsageSettingsState> {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
216
|
+
path: string,
|
|
217
|
+
providerId: string,
|
|
218
|
+
targetId: string,
|
|
219
|
+
operations: UsageSettingsFileOperations,
|
|
220
|
+
signal?: AbortSignal,
|
|
248
221
|
): Promise<{ saved: UsageSettingsState; previous: UsageSettingsState }> {
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
243
|
+
path: string,
|
|
244
|
+
mutate: (document: Record<string, unknown>) => void,
|
|
245
|
+
operations: UsageSettingsFileOperations,
|
|
246
|
+
signal?: AbortSignal,
|
|
247
|
+
expected?: UsageSettingsState,
|
|
275
248
|
): Promise<UsageSettingsState> {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
289
|
+
path: string,
|
|
290
|
+
published: UsageSettingsState,
|
|
291
|
+
previous: UsageSettingsState,
|
|
292
|
+
operations: UsageSettingsFileOperations,
|
|
320
293
|
): Promise<void> {
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
-
|
|
346
|
-
|
|
318
|
+
left: Pick<UsageSettingsState, "kind" | "document">,
|
|
319
|
+
right: Pick<UsageSettingsState, "kind" | "document">,
|
|
347
320
|
): boolean {
|
|
348
|
-
|
|
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
|
-
|
|
325
|
+
await chmod(path, 0o600);
|
|
355
326
|
}
|
|
356
327
|
|
|
357
328
|
function throwIfAborted(signal?: AbortSignal): void {
|
|
358
|
-
|
|
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
|
-
|
|
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
|
-
|
|
337
|
+
return error instanceof Error && "code" in error;
|
|
367
338
|
}
|
|
368
339
|
|
|
369
340
|
function normalizeSelectedTargets(value: unknown): Record<string, string> | undefined {
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
352
|
+
return /^[A-Za-z0-9][A-Za-z0-9._~-]{0,127}$/u.test(value);
|
|
382
353
|
}
|