@eiei114/pi-sub-core 1.5.1

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.
Files changed (52) hide show
  1. package/CHANGELOG.md +190 -0
  2. package/README.md +178 -0
  3. package/index.ts +540 -0
  4. package/package.json +35 -0
  5. package/src/cache.ts +546 -0
  6. package/src/config.ts +35 -0
  7. package/src/dependencies.ts +37 -0
  8. package/src/errors.ts +71 -0
  9. package/src/paths.ts +55 -0
  10. package/src/provider.ts +66 -0
  11. package/src/providers/detection.ts +51 -0
  12. package/src/providers/impl/anthropic.ts +174 -0
  13. package/src/providers/impl/antigravity.ts +226 -0
  14. package/src/providers/impl/codex.ts +186 -0
  15. package/src/providers/impl/copilot.ts +176 -0
  16. package/src/providers/impl/gemini.ts +130 -0
  17. package/src/providers/impl/kiro.ts +92 -0
  18. package/src/providers/impl/zai.ts +120 -0
  19. package/src/providers/index.ts +5 -0
  20. package/src/providers/metadata.ts +16 -0
  21. package/src/providers/registry.ts +54 -0
  22. package/src/providers/settings.ts +109 -0
  23. package/src/providers/status.ts +25 -0
  24. package/src/settings/behavior.ts +58 -0
  25. package/src/settings/menu.ts +83 -0
  26. package/src/settings/tools.ts +38 -0
  27. package/src/settings/ui.ts +450 -0
  28. package/src/settings-types.ts +95 -0
  29. package/src/settings-ui.ts +1 -0
  30. package/src/settings.ts +137 -0
  31. package/src/status.ts +245 -0
  32. package/src/storage/lock.ts +150 -0
  33. package/src/storage.ts +61 -0
  34. package/src/types.ts +33 -0
  35. package/src/ui/keybindings.ts +92 -0
  36. package/src/ui/settings-list.ts +290 -0
  37. package/src/usage/controller.ts +250 -0
  38. package/src/usage/fetch.ts +215 -0
  39. package/src/usage/types.ts +5 -0
  40. package/src/utils.ts +158 -0
  41. package/test/all.test.ts +9 -0
  42. package/test/cache.test.ts +157 -0
  43. package/test/controller.test.ts +101 -0
  44. package/test/detection.test.ts +24 -0
  45. package/test/extension.test.ts +233 -0
  46. package/test/helpers.ts +48 -0
  47. package/test/keybindings.test.ts +59 -0
  48. package/test/lock.test.ts +49 -0
  49. package/test/prioritize.test.ts +81 -0
  50. package/test/providers.test.ts +385 -0
  51. package/test/status.test.ts +70 -0
  52. package/tsconfig.json +5 -0
@@ -0,0 +1,290 @@
1
+ import type { Component, SettingItem, SettingsListTheme } from "@mariozechner/pi-tui";
2
+ import {
3
+ Input,
4
+ fuzzyFilter,
5
+ truncateToWidth,
6
+ visibleWidth,
7
+ wrapTextWithAnsi,
8
+ } from "@mariozechner/pi-tui";
9
+ import { getSettingsKeybindings } from "./keybindings.js";
10
+
11
+ export interface SettingsListOptions {
12
+ enableSearch?: boolean;
13
+ }
14
+
15
+ export const CUSTOM_OPTION = "__custom__";
16
+ export const CUSTOM_LABEL = "custom";
17
+
18
+ export type { SettingItem, SettingsListTheme };
19
+
20
+ export class SettingsList implements Component {
21
+ private items: SettingItem[];
22
+ private filteredItems: SettingItem[];
23
+ private theme: SettingsListTheme;
24
+ private selectedIndex = 0;
25
+ private maxVisible: number;
26
+ private onChange: (id: string, newValue: string) => void;
27
+ private onCancel: () => void;
28
+ private searchInput?: Input;
29
+ private searchEnabled: boolean;
30
+ private submenuComponent: Component | null = null;
31
+ private submenuItemIndex: number | null = null;
32
+
33
+ constructor(
34
+ items: SettingItem[],
35
+ maxVisible: number,
36
+ theme: SettingsListTheme,
37
+ onChange: (id: string, newValue: string) => void,
38
+ onCancel: () => void,
39
+ options: SettingsListOptions = {},
40
+ ) {
41
+ this.items = items;
42
+ this.filteredItems = items;
43
+ this.maxVisible = maxVisible;
44
+ this.theme = theme;
45
+ this.onChange = onChange;
46
+ this.onCancel = onCancel;
47
+ this.searchEnabled = options.enableSearch ?? false;
48
+
49
+ if (this.searchEnabled) {
50
+ this.searchInput = new Input();
51
+ }
52
+ }
53
+
54
+ /** Update an item's currentValue */
55
+ updateValue(id: string, newValue: string): void {
56
+ const item = this.items.find((i) => i.id === id);
57
+ if (item) {
58
+ item.currentValue = newValue;
59
+ }
60
+ }
61
+
62
+ invalidate(): void {
63
+ this.submenuComponent?.invalidate?.();
64
+ }
65
+
66
+ render(width: number): string[] {
67
+ // If submenu is active, render it instead
68
+ if (this.submenuComponent) {
69
+ return this.submenuComponent.render(width);
70
+ }
71
+ return this.renderMainList(width);
72
+ }
73
+
74
+ private renderMainList(width: number): string[] {
75
+ const lines: string[] = [];
76
+ if (this.searchEnabled && this.searchInput) {
77
+ lines.push(...this.searchInput.render(width));
78
+ lines.push("");
79
+ }
80
+
81
+ if (this.items.length === 0) {
82
+ lines.push(this.theme.hint(" No settings available"));
83
+ if (this.searchEnabled) {
84
+ this.addHintLine(lines);
85
+ }
86
+ return lines;
87
+ }
88
+
89
+ const displayItems = this.searchEnabled ? this.filteredItems : this.items;
90
+ if (displayItems.length === 0) {
91
+ lines.push(this.theme.hint(" No matching settings"));
92
+ this.addHintLine(lines);
93
+ return lines;
94
+ }
95
+
96
+ // Calculate visible range with scrolling
97
+ const startIndex = Math.max(
98
+ 0,
99
+ Math.min(
100
+ this.selectedIndex - Math.floor(this.maxVisible / 2),
101
+ displayItems.length - this.maxVisible,
102
+ ),
103
+ );
104
+ const endIndex = Math.min(startIndex + this.maxVisible, displayItems.length);
105
+
106
+ // Calculate max label width for alignment
107
+ const maxLabelWidth = Math.min(30, Math.max(...this.items.map((item) => visibleWidth(item.label))));
108
+
109
+ // Render visible items
110
+ for (let i = startIndex; i < endIndex; i++) {
111
+ const item = displayItems[i];
112
+ if (!item) continue;
113
+ const isSelected = i === this.selectedIndex;
114
+ const prefix = isSelected ? this.theme.cursor : " ";
115
+ const prefixWidth = visibleWidth(prefix);
116
+
117
+ // Pad label to align values
118
+ const labelPadded = item.label + " ".repeat(Math.max(0, maxLabelWidth - visibleWidth(item.label)));
119
+ const labelText = this.theme.label(labelPadded, isSelected);
120
+
121
+ // Calculate space for value
122
+ const separator = " ";
123
+ const usedWidth = prefixWidth + maxLabelWidth + visibleWidth(separator);
124
+ const valueMaxWidth = Math.max(1, width - usedWidth - 2);
125
+ const optionLines = isSelected && item.values && item.values.length > 0
126
+ ? wrapTextWithAnsi(this.formatOptionsInline(item, item.values), valueMaxWidth)
127
+ : null;
128
+ const valueText = optionLines
129
+ ? optionLines[0] ?? ""
130
+ : this.theme.value(truncateToWidth(item.currentValue, valueMaxWidth, ""), isSelected);
131
+ const line = prefix + labelText + separator + valueText;
132
+ lines.push(truncateToWidth(line, width, ""));
133
+ if (optionLines && optionLines.length > 1) {
134
+ const indent = " ".repeat(prefixWidth + maxLabelWidth + visibleWidth(separator));
135
+ for (const continuation of optionLines.slice(1)) {
136
+ lines.push(truncateToWidth(indent + continuation, width, ""));
137
+ }
138
+ }
139
+ }
140
+
141
+ // Add scroll indicator if needed
142
+ if (startIndex > 0 || endIndex < displayItems.length) {
143
+ const scrollText = ` (${this.selectedIndex + 1}/${displayItems.length})`;
144
+ lines.push(this.theme.hint(truncateToWidth(scrollText, width - 2, "")));
145
+ }
146
+
147
+ // Add description for selected item
148
+ const selectedItem = displayItems[this.selectedIndex];
149
+ if (selectedItem?.description) {
150
+ lines.push("");
151
+ const wrapWidth = Math.max(1, width - 4);
152
+ const wrappedDesc = wrapTextWithAnsi(selectedItem.description, wrapWidth);
153
+ for (const line of wrappedDesc) {
154
+ const prefixed = ` ${line}`;
155
+ lines.push(this.theme.description(truncateToWidth(prefixed, width, "")));
156
+ }
157
+ }
158
+
159
+
160
+ // Add hint
161
+ this.addHintLine(lines);
162
+ return lines;
163
+ }
164
+
165
+ handleInput(data: string): void {
166
+ // If submenu is active, delegate all input to it
167
+ // The submenu's onCancel (triggered by escape) will call done() which closes it
168
+ if (this.submenuComponent) {
169
+ this.submenuComponent.handleInput?.(data);
170
+ return;
171
+ }
172
+
173
+ const kb = getSettingsKeybindings();
174
+ const displayItems = this.searchEnabled ? this.filteredItems : this.items;
175
+
176
+ if (kb.matches(data, "selectUp")) {
177
+ if (displayItems.length === 0) return;
178
+ this.selectedIndex = this.selectedIndex === 0 ? displayItems.length - 1 : this.selectedIndex - 1;
179
+ } else if (kb.matches(data, "selectDown")) {
180
+ if (displayItems.length === 0) return;
181
+ this.selectedIndex = this.selectedIndex === displayItems.length - 1 ? 0 : this.selectedIndex + 1;
182
+ } else if (kb.matches(data, "cursorLeft")) {
183
+ this.stepValue(-1);
184
+ } else if (kb.matches(data, "cursorRight")) {
185
+ this.stepValue(1);
186
+ } else if (kb.matches(data, "selectConfirm") || data === " ") {
187
+ this.activateItem();
188
+ } else if (kb.matches(data, "selectCancel")) {
189
+ this.onCancel();
190
+ } else if (this.searchEnabled && this.searchInput) {
191
+ const sanitized = data.replace(/ /g, "");
192
+ if (!sanitized) {
193
+ return;
194
+ }
195
+ this.searchInput.handleInput(sanitized);
196
+ this.applyFilter(this.searchInput.getValue());
197
+ }
198
+ }
199
+
200
+ private stepValue(direction: -1 | 1): void {
201
+ const displayItems = this.searchEnabled ? this.filteredItems : this.items;
202
+ const item = displayItems[this.selectedIndex];
203
+ if (!item || !item.values || item.values.length === 0) return;
204
+ const values = item.values;
205
+ let currentIndex = values.indexOf(item.currentValue);
206
+ if (currentIndex === -1) {
207
+ currentIndex = direction > 0 ? 0 : values.length - 1;
208
+ }
209
+ const nextIndex = (currentIndex + direction + values.length) % values.length;
210
+ const newValue = values[nextIndex];
211
+ if (newValue === CUSTOM_OPTION) {
212
+ item.currentValue = newValue;
213
+ this.onChange(item.id, newValue);
214
+ return;
215
+ }
216
+ item.currentValue = newValue;
217
+ this.onChange(item.id, newValue);
218
+ }
219
+
220
+ private activateItem(): void {
221
+ const item = this.searchEnabled ? this.filteredItems[this.selectedIndex] : this.items[this.selectedIndex];
222
+ if (!item) return;
223
+
224
+ const hasCustom = Boolean(item.values && item.values.includes(CUSTOM_OPTION));
225
+ const currentIsCustom = hasCustom && item.values && !item.values.includes(item.currentValue);
226
+
227
+ if (item.submenu && hasCustom) {
228
+ if (currentIsCustom || item.currentValue === CUSTOM_OPTION) {
229
+ this.openSubmenu(item);
230
+ }
231
+ return;
232
+ }
233
+
234
+ if (item.submenu) {
235
+ this.openSubmenu(item);
236
+ }
237
+ }
238
+
239
+ private closeSubmenu(): void {
240
+ this.submenuComponent = null;
241
+ // Restore selection to the item that opened the submenu
242
+ if (this.submenuItemIndex !== null) {
243
+ this.selectedIndex = this.submenuItemIndex;
244
+ this.submenuItemIndex = null;
245
+ }
246
+ }
247
+
248
+ private applyFilter(query: string): void {
249
+ this.filteredItems = fuzzyFilter(this.items, query, (item) => item.label);
250
+ this.selectedIndex = 0;
251
+ }
252
+
253
+ private formatOptionsInline(item: SettingItem, values: string[]): string {
254
+ const separator = this.theme.description(" • ");
255
+ const hasCustom = values.includes(CUSTOM_OPTION);
256
+ const currentIsCustom = hasCustom && !values.includes(item.currentValue);
257
+ return values
258
+ .map((value) => {
259
+ const label = value === CUSTOM_OPTION
260
+ ? (currentIsCustom ? `${CUSTOM_LABEL} (${item.currentValue})` : CUSTOM_LABEL)
261
+ : value;
262
+ const selected = value === item.currentValue || (currentIsCustom && value === CUSTOM_OPTION);
263
+ return this.theme.value(label, selected);
264
+ })
265
+ .join(separator);
266
+ }
267
+
268
+ private openSubmenu(item: SettingItem): void {
269
+ if (!item.submenu) return;
270
+ this.submenuItemIndex = this.selectedIndex;
271
+ this.submenuComponent = item.submenu(item.currentValue, (selectedValue) => {
272
+ if (selectedValue !== undefined) {
273
+ item.currentValue = selectedValue;
274
+ this.onChange(item.id, selectedValue);
275
+ }
276
+ this.closeSubmenu();
277
+ });
278
+ }
279
+
280
+ private addHintLine(lines: string[]): void {
281
+ lines.push("");
282
+ lines.push(
283
+ this.theme.hint(
284
+ this.searchEnabled
285
+ ? " Type to search · ←/→ change · Enter/Space edit custom · Esc to cancel"
286
+ : " ←/→ change · Enter/Space edit custom · Esc to cancel",
287
+ ),
288
+ );
289
+ }
290
+ }
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Usage refresh and provider selection controller.
3
+ */
4
+
5
+ import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
6
+ import type { ProviderName, UsageSnapshot } from "../types.js";
7
+ import type { Settings } from "../settings-types.js";
8
+ import { detectProviderFromModel } from "../providers/detection.js";
9
+ import { isExpectedMissingData } from "../errors.js";
10
+ import { formatElapsedSince } from "../utils.js";
11
+ import { fetchUsageForProvider, refreshStatusForProvider } from "./fetch.js";
12
+ import type { Dependencies } from "../types.js";
13
+ import { getCachedData, readCache } from "../cache.js";
14
+ import { hasProviderCredentials } from "../providers/registry.js";
15
+
16
+ export interface UsageControllerState {
17
+ currentProvider?: ProviderName;
18
+ cachedUsage?: UsageSnapshot;
19
+ lastSuccessAt?: number;
20
+ providerCycleIndex: number;
21
+ }
22
+
23
+ export interface UsageUpdate {
24
+ provider?: ProviderName;
25
+ usage?: UsageSnapshot;
26
+ }
27
+
28
+ export type UsageUpdateHandler = (update: UsageUpdate) => void;
29
+
30
+ export function createUsageController(deps: Dependencies) {
31
+ function isProviderAvailable(
32
+ settings: Settings,
33
+ provider: ProviderName,
34
+ options?: { skipCredentials?: boolean }
35
+ ): boolean {
36
+ const setting = settings.providers[provider];
37
+ if (setting.enabled === "off" || setting.enabled === false) return false;
38
+ if (setting.enabled === "on" || setting.enabled === true) return true;
39
+ if (options?.skipCredentials) return true;
40
+ return hasProviderCredentials(provider, deps);
41
+ }
42
+
43
+ function getEnabledProviders(settings: Settings): ProviderName[] {
44
+ return settings.providerOrder.filter((p) => isProviderAvailable(settings, p));
45
+ }
46
+
47
+ function resolveProvider(
48
+ ctx: ExtensionContext,
49
+ settings: Settings,
50
+ state: UsageControllerState,
51
+ options?: { skipCredentials?: boolean }
52
+ ): ProviderName | undefined {
53
+ const detected = detectProviderFromModel(ctx.model);
54
+ if (detected && isProviderAvailable(settings, detected, options)) {
55
+ return detected;
56
+ }
57
+ return undefined;
58
+ }
59
+
60
+ function emitUpdate(state: UsageControllerState, onUpdate: UsageUpdateHandler): void {
61
+ onUpdate({
62
+ provider: state.currentProvider,
63
+ usage: state.cachedUsage,
64
+ });
65
+ }
66
+
67
+ async function refresh(
68
+ ctx: ExtensionContext,
69
+ settings: Settings,
70
+ state: UsageControllerState,
71
+ onUpdate: UsageUpdateHandler,
72
+ options?: { force?: boolean; allowStaleCache?: boolean; forceStatus?: boolean; skipFetch?: boolean }
73
+ ): Promise<void> {
74
+ const provider = resolveProvider(ctx, settings, state, { skipCredentials: options?.skipFetch });
75
+ if (!provider) {
76
+ state.currentProvider = undefined;
77
+ state.cachedUsage = undefined;
78
+ emitUpdate(state, onUpdate);
79
+ return;
80
+ }
81
+
82
+ const providerChanged = provider !== state.currentProvider;
83
+ state.currentProvider = provider;
84
+ if (providerChanged) {
85
+ state.cachedUsage = undefined;
86
+ }
87
+
88
+ const cache = readCache();
89
+ let cachedEntry = await getCachedData(provider, settings.behavior.refreshInterval * 1000, cache);
90
+ if (!cachedEntry && options?.allowStaleCache) {
91
+ cachedEntry = cache[provider] ?? null;
92
+ }
93
+ if (cachedEntry?.usage) {
94
+ state.cachedUsage = {
95
+ ...cachedEntry.usage,
96
+ status: cachedEntry.status,
97
+ lastSuccessAt: cachedEntry.fetchedAt,
98
+ };
99
+ if (!cachedEntry.usage.error) {
100
+ state.lastSuccessAt = cachedEntry.fetchedAt;
101
+ }
102
+ }
103
+ emitUpdate(state, onUpdate);
104
+
105
+ if (options?.skipFetch) {
106
+ return;
107
+ }
108
+
109
+ const result = await fetchUsageForProvider(deps, settings, provider, options);
110
+ const error = result.usage?.error;
111
+ const fetchError = Boolean(error && !isExpectedMissingData(error));
112
+ if (fetchError) {
113
+ let fallback = state.cachedUsage;
114
+ let fallbackFetchedAt = state.lastSuccessAt;
115
+ if (!fallback || fallback.windows.length === 0) {
116
+ const cachedEntry = cache[provider];
117
+ const cachedUsage = cachedEntry?.usage ? { ...cachedEntry.usage, status: cachedEntry.status } : undefined;
118
+ fallback = cachedUsage && cachedUsage.windows.length > 0 ? cachedUsage : undefined;
119
+ if (cachedEntry?.fetchedAt) fallbackFetchedAt = cachedEntry.fetchedAt;
120
+ }
121
+ if (fallback && fallback.windows.length > 0) {
122
+ const lastSuccessAt = fallbackFetchedAt ?? state.lastSuccessAt;
123
+ const elapsed = lastSuccessAt ? formatElapsedSince(lastSuccessAt) : undefined;
124
+ const description = elapsed ? (elapsed === "just now" ? "just now" : `${elapsed} ago`) : "Fetch failed";
125
+ state.cachedUsage = {
126
+ ...fallback,
127
+ lastSuccessAt,
128
+ error,
129
+ status: { indicator: "minor", description },
130
+ };
131
+ } else {
132
+ state.cachedUsage = result.usage ? { ...result.usage, status: result.status } : undefined;
133
+ }
134
+ } else {
135
+ const successAt = Date.now();
136
+ state.cachedUsage = result.usage
137
+ ? { ...result.usage, status: result.status, lastSuccessAt: successAt }
138
+ : undefined;
139
+ if (result.usage && !result.usage.error) {
140
+ state.lastSuccessAt = successAt;
141
+ }
142
+ }
143
+ emitUpdate(state, onUpdate);
144
+ }
145
+
146
+ async function refreshStatus(
147
+ ctx: ExtensionContext,
148
+ settings: Settings,
149
+ state: UsageControllerState,
150
+ onUpdate: UsageUpdateHandler,
151
+ options?: { force?: boolean; allowStaleCache?: boolean; skipFetch?: boolean }
152
+ ): Promise<void> {
153
+ const provider = resolveProvider(ctx, settings, state, { skipCredentials: options?.skipFetch });
154
+ if (!provider) {
155
+ state.currentProvider = undefined;
156
+ state.cachedUsage = undefined;
157
+ emitUpdate(state, onUpdate);
158
+ return;
159
+ }
160
+
161
+ const providerChanged = provider !== state.currentProvider;
162
+ state.currentProvider = provider;
163
+ if (providerChanged) {
164
+ state.cachedUsage = undefined;
165
+ }
166
+
167
+ const cache = readCache();
168
+ let cachedEntry = await getCachedData(provider, settings.behavior.refreshInterval * 1000, cache);
169
+ if (!cachedEntry && options?.allowStaleCache) {
170
+ cachedEntry = cache[provider] ?? null;
171
+ }
172
+ if (cachedEntry?.usage) {
173
+ state.cachedUsage = {
174
+ ...cachedEntry.usage,
175
+ status: cachedEntry.status,
176
+ lastSuccessAt: cachedEntry.fetchedAt,
177
+ };
178
+ if (!cachedEntry.usage.error) {
179
+ state.lastSuccessAt = cachedEntry.fetchedAt;
180
+ }
181
+ }
182
+
183
+ if (options?.skipFetch) {
184
+ emitUpdate(state, onUpdate);
185
+ return;
186
+ }
187
+
188
+ const status = await refreshStatusForProvider(deps, settings, provider, { force: options?.force });
189
+ if (status && state.cachedUsage) {
190
+ state.cachedUsage = { ...state.cachedUsage, status };
191
+ }
192
+
193
+ emitUpdate(state, onUpdate);
194
+ }
195
+
196
+ async function cycleProvider(
197
+ ctx: ExtensionContext,
198
+ settings: Settings,
199
+ state: UsageControllerState,
200
+ onUpdate: UsageUpdateHandler
201
+ ): Promise<void> {
202
+ const enabledProviders = getEnabledProviders(settings);
203
+ if (enabledProviders.length === 0) {
204
+ state.currentProvider = undefined;
205
+ state.cachedUsage = undefined;
206
+ emitUpdate(state, onUpdate);
207
+ return;
208
+ }
209
+
210
+ const currentIndex = state.currentProvider
211
+ ? enabledProviders.indexOf(state.currentProvider)
212
+ : -1;
213
+ if (currentIndex >= 0) {
214
+ state.providerCycleIndex = currentIndex;
215
+ }
216
+
217
+ const total = enabledProviders.length;
218
+ for (let i = 0; i < total; i += 1) {
219
+ state.providerCycleIndex = (state.providerCycleIndex + 1) % total;
220
+ const nextProvider = enabledProviders[state.providerCycleIndex];
221
+ const result = await fetchUsageForProvider(deps, settings, nextProvider);
222
+ if (!isUsageAvailable(result.usage)) {
223
+ continue;
224
+ }
225
+ state.currentProvider = nextProvider;
226
+ state.cachedUsage = result.usage ? { ...result.usage, status: result.status } : undefined;
227
+ emitUpdate(state, onUpdate);
228
+ return;
229
+ }
230
+
231
+ state.currentProvider = undefined;
232
+ state.cachedUsage = undefined;
233
+ emitUpdate(state, onUpdate);
234
+ }
235
+
236
+ function isUsageAvailable(usage: UsageSnapshot | undefined): usage is UsageSnapshot {
237
+ if (!usage) return false;
238
+ if (usage.windows.length > 0) return true;
239
+ if (!usage.error) return false;
240
+ return !isExpectedMissingData(usage.error);
241
+ }
242
+
243
+ return {
244
+ getEnabledProviders,
245
+ resolveProvider,
246
+ refresh,
247
+ refreshStatus,
248
+ cycleProvider,
249
+ };
250
+ }