ogi-addon 1.9.5 → 2.0.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/build/{Configuration-CdRZbO6z.d.mts → Configuration-DdkCGFMU.d.cts} +2 -2
- package/build/{Configuration-WeOm-F0_.d.cts → Configuration-fDtr2bmH.d.mts} +2 -2
- package/build/{ConfigurationBuilder-BbZDA_xx.d.mts → ConfigurationBuilder-C83EP5v2.d.cts} +81 -21
- package/build/{ConfigurationBuilder-BSuJ4rSI.cjs → ConfigurationBuilder-CFXi6UwU.cjs} +112 -14
- package/build/ConfigurationBuilder-CFXi6UwU.cjs.map +1 -0
- package/build/{ConfigurationBuilder-CfHLKMTO.d.cts → ConfigurationBuilder-lzKf9gHw.d.mts} +81 -21
- package/build/{EventResponse-CQhmdz3C.d.mts → EventResponse-D0TZjAVC.d.mts} +88 -31
- package/build/{EventResponse-D1c-Df5W.d.cts → EventResponse-DgSuJPu8.d.cts} +88 -31
- package/build/EventResponse.cjs +5 -4
- package/build/EventResponse.cjs.map +1 -1
- package/build/EventResponse.d.cts +1 -1
- package/build/EventResponse.d.mts +1 -1
- package/build/EventResponse.mjs +5 -4
- package/build/EventResponse.mjs.map +1 -1
- package/build/{SearchEngine-CRQWXfo6.d.mts → SearchEngine-Cn_-M-at.d.cts} +5 -2
- package/build/{SearchEngine-DBSUNM4A.d.cts → SearchEngine-lZioNunY.d.mts} +5 -2
- package/build/SearchEngine.d.cts +1 -1
- package/build/SearchEngine.d.mts +1 -1
- package/build/config/Configuration.cjs +6 -3
- package/build/config/Configuration.cjs.map +1 -1
- package/build/config/Configuration.d.cts +3 -3
- package/build/config/Configuration.d.mts +3 -3
- package/build/config/Configuration.mjs +5 -4
- package/build/config/Configuration.mjs.map +1 -1
- package/build/config/ConfigurationBuilder.cjs +3 -1
- package/build/config/ConfigurationBuilder.d.cts +2 -2
- package/build/config/ConfigurationBuilder.d.mts +2 -2
- package/build/config/ConfigurationBuilder.mjs +100 -14
- package/build/config/ConfigurationBuilder.mjs.map +1 -1
- package/build/main.cjs +116 -14
- package/build/main.cjs.map +1 -1
- package/build/main.d.cts +5 -5
- package/build/main.d.mts +5 -5
- package/build/main.mjs +115 -14
- package/build/main.mjs.map +1 -1
- package/package.json +1 -1
- package/src/EventResponse.ts +14 -13
- package/src/SearchEngine.ts +5 -1
- package/src/config/Configuration.ts +13 -3
- package/src/config/ConfigurationBuilder.ts +157 -41
- package/src/main.ts +194 -36
- package/build/ConfigurationBuilder-BSuJ4rSI.cjs.map +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import z, { ZodError } from 'zod';
|
|
2
2
|
|
|
3
3
|
export interface ConfigurationFile {
|
|
4
|
-
[key: string]: ConfigurationOption
|
|
4
|
+
[key: string]: ConfigurationOption<string>;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
7
|
const configValidation = z.object({
|
|
@@ -10,61 +10,98 @@ const configValidation = z.object({
|
|
|
10
10
|
description: z.string().min(1),
|
|
11
11
|
});
|
|
12
12
|
|
|
13
|
-
export function isStringOption(
|
|
14
|
-
option: ConfigurationOption
|
|
15
|
-
): option is StringOption {
|
|
13
|
+
export function isStringOption<N extends string = string>(
|
|
14
|
+
option: ConfigurationOption<N>
|
|
15
|
+
): option is StringOption<N> {
|
|
16
16
|
return option.type === 'string';
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
export function isNumberOption(
|
|
20
|
-
option: ConfigurationOption
|
|
21
|
-
): option is NumberOption {
|
|
19
|
+
export function isNumberOption<N extends string = string>(
|
|
20
|
+
option: ConfigurationOption<N>
|
|
21
|
+
): option is NumberOption<N> {
|
|
22
22
|
return option.type === 'number';
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
export function isBooleanOption(
|
|
26
|
-
option: ConfigurationOption
|
|
27
|
-
): option is BooleanOption {
|
|
25
|
+
export function isBooleanOption<N extends string = string>(
|
|
26
|
+
option: ConfigurationOption<N>
|
|
27
|
+
): option is BooleanOption<N> {
|
|
28
28
|
return option.type === 'boolean';
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
export
|
|
32
|
-
|
|
31
|
+
export function isActionOption<N extends string = string>(
|
|
32
|
+
option: ConfigurationOption<N>
|
|
33
|
+
): option is ActionOption<N> {
|
|
34
|
+
return option.type === 'action';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* A builder for creating configuration screens. The generic type T accumulates
|
|
39
|
+
* the types of all options added to the builder, enabling type-safe access to
|
|
40
|
+
* the configuration values.
|
|
41
|
+
*
|
|
42
|
+
* @template T - The accumulated type of all configuration options
|
|
43
|
+
*/
|
|
44
|
+
export class ConfigurationBuilder<
|
|
45
|
+
T extends Record<string, string | number | boolean> = {}
|
|
46
|
+
> {
|
|
47
|
+
private options: ConfigurationOption<string>[] = [];
|
|
33
48
|
|
|
34
49
|
/**
|
|
35
50
|
* Add a number option to the configuration builder and return the builder for chaining. You must provide a name, display name, and description for the option.
|
|
36
|
-
* @param option { (option: NumberOption) => NumberOption }
|
|
37
|
-
* @returns
|
|
51
|
+
* @param option { (option: NumberOption) => NumberOption<K> }
|
|
52
|
+
* @returns A new ConfigurationBuilder with the number option's type added
|
|
38
53
|
*/
|
|
39
|
-
public addNumberOption(
|
|
40
|
-
option: (option: NumberOption) => NumberOption
|
|
41
|
-
): ConfigurationBuilder {
|
|
54
|
+
public addNumberOption<K extends string>(
|
|
55
|
+
option: (option: NumberOption) => NumberOption<K>
|
|
56
|
+
): ConfigurationBuilder<T & { [P in K]: number }> {
|
|
42
57
|
let newOption = new NumberOption();
|
|
43
|
-
|
|
44
|
-
this.options.push(
|
|
45
|
-
return this
|
|
58
|
+
const configuredOption = option(newOption);
|
|
59
|
+
this.options.push(configuredOption);
|
|
60
|
+
return this as unknown as ConfigurationBuilder<T & { [P in K]: number }>;
|
|
46
61
|
}
|
|
47
62
|
|
|
48
63
|
/**
|
|
49
64
|
* Add a string option to the configuration builder and return the builder for chaining. You must provide a name, display name, and description for the option.
|
|
50
|
-
* @param option { (option: StringOption) => StringOption }
|
|
65
|
+
* @param option { (option: StringOption) => StringOption<K> }
|
|
66
|
+
* @returns A new ConfigurationBuilder with the string option's type added
|
|
51
67
|
*/
|
|
52
|
-
public addStringOption
|
|
68
|
+
public addStringOption<K extends string>(
|
|
69
|
+
option: (option: StringOption) => StringOption<K>
|
|
70
|
+
): ConfigurationBuilder<T & { [P in K]: string }> {
|
|
53
71
|
let newOption = new StringOption();
|
|
54
|
-
|
|
55
|
-
this.options.push(
|
|
56
|
-
return this
|
|
72
|
+
const configuredOption = option(newOption);
|
|
73
|
+
this.options.push(configuredOption);
|
|
74
|
+
return this as unknown as ConfigurationBuilder<T & { [P in K]: string }>;
|
|
57
75
|
}
|
|
58
76
|
|
|
59
77
|
/**
|
|
60
78
|
* Add a boolean option to the configuration builder and return the builder for chaining. You must provide a name, display name, and description for the option.
|
|
61
|
-
* @param option { (option: BooleanOption) => BooleanOption }
|
|
79
|
+
* @param option { (option: BooleanOption) => BooleanOption<K> }
|
|
80
|
+
* @returns A new ConfigurationBuilder with the boolean option's type added
|
|
62
81
|
*/
|
|
63
|
-
public addBooleanOption
|
|
82
|
+
public addBooleanOption<K extends string>(
|
|
83
|
+
option: (option: BooleanOption) => BooleanOption<K>
|
|
84
|
+
): ConfigurationBuilder<T & { [P in K]: boolean }> {
|
|
64
85
|
let newOption = new BooleanOption();
|
|
65
|
-
|
|
66
|
-
this.options.push(
|
|
67
|
-
return this
|
|
86
|
+
const configuredOption = option(newOption);
|
|
87
|
+
this.options.push(configuredOption);
|
|
88
|
+
return this as unknown as ConfigurationBuilder<T & { [P in K]: boolean }>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Add an action option to the configuration builder and return the builder for chaining.
|
|
93
|
+
* Action options contribute a boolean to the return type (true if clicked, false if not).
|
|
94
|
+
* You must provide a name, display name, and description for the option.
|
|
95
|
+
* @param option { (option: ActionOption) => ActionOption<K> }
|
|
96
|
+
* @returns A new ConfigurationBuilder with the action option's type added as boolean
|
|
97
|
+
*/
|
|
98
|
+
public addActionOption<K extends string>(
|
|
99
|
+
option: (option: ActionOption) => ActionOption<K>
|
|
100
|
+
): ConfigurationBuilder<T & { [P in K]: boolean }> {
|
|
101
|
+
let newOption = new ActionOption();
|
|
102
|
+
const configuredOption = option(newOption);
|
|
103
|
+
this.options.push(configuredOption);
|
|
104
|
+
return this as unknown as ConfigurationBuilder<T & { [P in K]: boolean }>;
|
|
68
105
|
}
|
|
69
106
|
|
|
70
107
|
public build(includeFunctions: boolean): ConfigurationFile {
|
|
@@ -87,9 +124,14 @@ export class ConfigurationBuilder {
|
|
|
87
124
|
}
|
|
88
125
|
}
|
|
89
126
|
|
|
90
|
-
export type ConfigurationOptionType =
|
|
91
|
-
|
|
92
|
-
|
|
127
|
+
export type ConfigurationOptionType =
|
|
128
|
+
| 'string'
|
|
129
|
+
| 'number'
|
|
130
|
+
| 'boolean'
|
|
131
|
+
| 'action'
|
|
132
|
+
| 'unset';
|
|
133
|
+
export class ConfigurationOption<N extends string = string> {
|
|
134
|
+
public name: N = '' as N;
|
|
93
135
|
public defaultValue: unknown = '';
|
|
94
136
|
public displayName: string = '';
|
|
95
137
|
public description: string = '';
|
|
@@ -99,9 +141,9 @@ export class ConfigurationOption {
|
|
|
99
141
|
* Set the name of the option. **REQUIRED**
|
|
100
142
|
* @param name {string} The name of the option. This is used to reference the option in the configuration file.
|
|
101
143
|
*/
|
|
102
|
-
setName(name:
|
|
103
|
-
this.name = name;
|
|
104
|
-
return this
|
|
144
|
+
setName<K extends string>(name: K): ConfigurationOption<K> {
|
|
145
|
+
this.name = name as unknown as N;
|
|
146
|
+
return this as unknown as ConfigurationOption<K>;
|
|
105
147
|
}
|
|
106
148
|
|
|
107
149
|
/**
|
|
@@ -109,7 +151,7 @@ export class ConfigurationOption {
|
|
|
109
151
|
* @param displayName {string} The display name of the option.
|
|
110
152
|
* @returns
|
|
111
153
|
*/
|
|
112
|
-
setDisplayName(displayName: string) {
|
|
154
|
+
setDisplayName(displayName: string): this {
|
|
113
155
|
this.displayName = displayName;
|
|
114
156
|
return this;
|
|
115
157
|
}
|
|
@@ -119,7 +161,7 @@ export class ConfigurationOption {
|
|
|
119
161
|
* @param description {string} The description of the option.
|
|
120
162
|
* @returns
|
|
121
163
|
*/
|
|
122
|
-
setDescription(description: string) {
|
|
164
|
+
setDescription(description: string): this {
|
|
123
165
|
this.description = description;
|
|
124
166
|
return this;
|
|
125
167
|
}
|
|
@@ -133,7 +175,7 @@ export class ConfigurationOption {
|
|
|
133
175
|
}
|
|
134
176
|
}
|
|
135
177
|
|
|
136
|
-
export class StringOption extends ConfigurationOption {
|
|
178
|
+
export class StringOption<N extends string = string> extends ConfigurationOption<N> {
|
|
137
179
|
public allowedValues: string[] = [];
|
|
138
180
|
public minTextLength: number = 0;
|
|
139
181
|
public maxTextLength: number = Number.MAX_SAFE_INTEGER;
|
|
@@ -141,6 +183,15 @@ export class StringOption extends ConfigurationOption {
|
|
|
141
183
|
public inputType: 'text' | 'file' | 'password' | 'folder' = 'text';
|
|
142
184
|
public type: ConfigurationOptionType = 'string';
|
|
143
185
|
|
|
186
|
+
/**
|
|
187
|
+
* Set the name of the option. **REQUIRED**
|
|
188
|
+
* @param name {string} The name of the option. This is used to reference the option in the configuration file.
|
|
189
|
+
*/
|
|
190
|
+
override setName<K extends string>(name: K): StringOption<K> {
|
|
191
|
+
this.name = name as unknown as N;
|
|
192
|
+
return this as unknown as StringOption<K>;
|
|
193
|
+
}
|
|
194
|
+
|
|
144
195
|
/**
|
|
145
196
|
* Set the allowed values for the string. If the array is empty, any value is allowed. When provided, the client will act like this option is a dropdown.
|
|
146
197
|
* @param allowedValues {string[]} An array of allowed values for the string. If the array is empty, any value is allowed.
|
|
@@ -215,13 +266,22 @@ export class StringOption extends ConfigurationOption {
|
|
|
215
266
|
}
|
|
216
267
|
}
|
|
217
268
|
|
|
218
|
-
export class NumberOption extends ConfigurationOption {
|
|
269
|
+
export class NumberOption<N extends string = string> extends ConfigurationOption<N> {
|
|
219
270
|
public min: number = 0;
|
|
220
271
|
public max: number = Number.MAX_SAFE_INTEGER;
|
|
221
272
|
public defaultValue: number = 0;
|
|
222
273
|
public type: ConfigurationOptionType = 'number';
|
|
223
274
|
public inputType: 'range' | 'number' = 'number';
|
|
224
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Set the name of the option. **REQUIRED**
|
|
278
|
+
* @param name {string} The name of the option. This is used to reference the option in the configuration file.
|
|
279
|
+
*/
|
|
280
|
+
override setName<K extends string>(name: K): NumberOption<K> {
|
|
281
|
+
this.name = name as unknown as N;
|
|
282
|
+
return this as unknown as NumberOption<K>;
|
|
283
|
+
}
|
|
284
|
+
|
|
225
285
|
/**
|
|
226
286
|
* Set the minimum value for the number. If the user provides a number that is less than this value, the validation will fail.
|
|
227
287
|
* @param min {number} The minimum value for the number.
|
|
@@ -272,10 +332,19 @@ export class NumberOption extends ConfigurationOption {
|
|
|
272
332
|
}
|
|
273
333
|
}
|
|
274
334
|
|
|
275
|
-
export class BooleanOption extends ConfigurationOption {
|
|
335
|
+
export class BooleanOption<N extends string = string> extends ConfigurationOption<N> {
|
|
276
336
|
public type: ConfigurationOptionType = 'boolean';
|
|
277
337
|
public defaultValue: boolean = false;
|
|
278
338
|
|
|
339
|
+
/**
|
|
340
|
+
* Set the name of the option. **REQUIRED**
|
|
341
|
+
* @param name {string} The name of the option. This is used to reference the option in the configuration file.
|
|
342
|
+
*/
|
|
343
|
+
override setName<K extends string>(name: K): BooleanOption<K> {
|
|
344
|
+
this.name = name as unknown as N;
|
|
345
|
+
return this as unknown as BooleanOption<K>;
|
|
346
|
+
}
|
|
347
|
+
|
|
279
348
|
/**
|
|
280
349
|
* Set the default value for the boolean. This value will be used if the user does not provide a value. **HIGHLY RECOMMENDED**
|
|
281
350
|
* @param defaultValue {boolean} The default value for the boolean.
|
|
@@ -292,3 +361,50 @@ export class BooleanOption extends ConfigurationOption {
|
|
|
292
361
|
return [true, ''];
|
|
293
362
|
}
|
|
294
363
|
}
|
|
364
|
+
|
|
365
|
+
export class ActionOption<N extends string = string> extends ConfigurationOption<N> {
|
|
366
|
+
public type: ConfigurationOptionType = 'action';
|
|
367
|
+
public manifest: Record<string, unknown> = {};
|
|
368
|
+
public buttonText: string = 'Run';
|
|
369
|
+
public taskName: string = '';
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Set the name of the option. **REQUIRED**
|
|
373
|
+
* @param name {string} The name of the option. This is used to reference the option in the configuration file.
|
|
374
|
+
*/
|
|
375
|
+
override setName<K extends string>(name: K): ActionOption<K> {
|
|
376
|
+
this.name = name as unknown as N;
|
|
377
|
+
return this as unknown as ActionOption<K>;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Set the task name that will be used to identify which task handler to run. This should match the name used in `addon.onTask()`.
|
|
382
|
+
* @param taskName {string} The task name to identify the handler.
|
|
383
|
+
*/
|
|
384
|
+
setTaskName(taskName: string): this {
|
|
385
|
+
this.taskName = taskName;
|
|
386
|
+
return this;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Set the manifest object that will be passed to the task-run handler. The task name should be set via setTaskName() rather than in the manifest.
|
|
391
|
+
* @param manifest {Record<string, unknown>} The manifest object to pass to the task handler.
|
|
392
|
+
*/
|
|
393
|
+
setManifest(manifest: Record<string, unknown>): this {
|
|
394
|
+
this.manifest = manifest;
|
|
395
|
+
return this;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Set the text displayed on the action button.
|
|
400
|
+
* @param text {string} The button text.
|
|
401
|
+
*/
|
|
402
|
+
setButtonText(text: string): this {
|
|
403
|
+
this.buttonText = text;
|
|
404
|
+
return this;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
override validate(_input: unknown): [boolean, string] {
|
|
408
|
+
return [true, ''];
|
|
409
|
+
}
|
|
410
|
+
}
|
package/src/main.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import ws, { WebSocket } from 'ws';
|
|
2
2
|
import events from 'node:events';
|
|
3
|
-
import {
|
|
4
|
-
ConfigurationBuilder,
|
|
5
|
-
} from './config/ConfigurationBuilder';
|
|
3
|
+
import { ConfigurationBuilder } from './config/ConfigurationBuilder';
|
|
6
4
|
import type { ConfigurationFile } from './config/ConfigurationBuilder';
|
|
7
5
|
import { Configuration } from './config/Configuration';
|
|
8
6
|
import EventResponse from './EventResponse';
|
|
@@ -20,7 +18,6 @@ export type OGIAddonEvent =
|
|
|
20
18
|
| 'game-details'
|
|
21
19
|
| 'exit'
|
|
22
20
|
| 'check-for-updates'
|
|
23
|
-
| 'task-run'
|
|
24
21
|
| 'request-dl'
|
|
25
22
|
| 'catalog';
|
|
26
23
|
|
|
@@ -73,7 +70,9 @@ export interface ClientSentEventTypes {
|
|
|
73
70
|
progress: number;
|
|
74
71
|
};
|
|
75
72
|
notification: Notification;
|
|
76
|
-
'input-asked': ConfigurationBuilder
|
|
73
|
+
'input-asked': ConfigurationBuilder<
|
|
74
|
+
Record<string, string | number | boolean>
|
|
75
|
+
>;
|
|
77
76
|
'task-update': {
|
|
78
77
|
id: string;
|
|
79
78
|
progress: number;
|
|
@@ -214,21 +213,6 @@ export interface EventListenerTypes {
|
|
|
214
213
|
event: EventResponse<BasicLibraryInfo[]>
|
|
215
214
|
) => void;
|
|
216
215
|
|
|
217
|
-
/**
|
|
218
|
-
* This event is emitted when the client requests for a task to be run. Addon should resolve the event with the task.
|
|
219
|
-
* @param task
|
|
220
|
-
* @param event
|
|
221
|
-
* @returns
|
|
222
|
-
*/
|
|
223
|
-
'task-run': (
|
|
224
|
-
task: {
|
|
225
|
-
manifest: Record<string, unknown>;
|
|
226
|
-
downloadPath: string;
|
|
227
|
-
name: string;
|
|
228
|
-
},
|
|
229
|
-
event: EventResponse<void>
|
|
230
|
-
) => void;
|
|
231
|
-
|
|
232
216
|
/**
|
|
233
217
|
* This event is emitted when the client requests for a game details to be fetched. Addon should resolve the event with the game details. This is used to generate a store page for the game.
|
|
234
218
|
* @param appID
|
|
@@ -360,6 +344,17 @@ export default class OGIAddon {
|
|
|
360
344
|
public config: Configuration = new Configuration({});
|
|
361
345
|
private eventsAvailable: OGIAddonEvent[] = [];
|
|
362
346
|
private registeredConnectEvent: boolean = false;
|
|
347
|
+
private taskHandlers: Map<
|
|
348
|
+
string,
|
|
349
|
+
(
|
|
350
|
+
task: Task,
|
|
351
|
+
data: {
|
|
352
|
+
manifest: Record<string, unknown>;
|
|
353
|
+
downloadPath: string;
|
|
354
|
+
name: string;
|
|
355
|
+
}
|
|
356
|
+
) => Promise<void> | void
|
|
357
|
+
> = new Map();
|
|
363
358
|
|
|
364
359
|
constructor(addonInfo: OGIAddonConfiguration) {
|
|
365
360
|
this.addonInfo = addonInfo;
|
|
@@ -451,6 +446,62 @@ export default class OGIAddon {
|
|
|
451
446
|
return task;
|
|
452
447
|
}
|
|
453
448
|
|
|
449
|
+
/**
|
|
450
|
+
* Register a task handler for a specific task name. The task name should match the taskName field in SearchResult or ActionOption.
|
|
451
|
+
* @param taskName {string} The name of the task (should match taskName in SearchResult or ActionOption.setTaskName()).
|
|
452
|
+
* @param handler {(task: Task, data: { manifest: Record<string, unknown>; downloadPath: string; name: string }) => Promise<void> | void} The handler function.
|
|
453
|
+
* @example
|
|
454
|
+
* ```typescript
|
|
455
|
+
* addon.onTask('clearCache', async (task) => {
|
|
456
|
+
* task.log('Clearing cache...');
|
|
457
|
+
* task.setProgress(50);
|
|
458
|
+
* await clearCacheFiles();
|
|
459
|
+
* task.setProgress(100);
|
|
460
|
+
* task.complete();
|
|
461
|
+
* });
|
|
462
|
+
* ```
|
|
463
|
+
*/
|
|
464
|
+
public onTask(
|
|
465
|
+
taskName: string,
|
|
466
|
+
handler: (
|
|
467
|
+
task: Task,
|
|
468
|
+
data: {
|
|
469
|
+
manifest: Record<string, unknown>;
|
|
470
|
+
downloadPath: string;
|
|
471
|
+
name: string;
|
|
472
|
+
}
|
|
473
|
+
) => Promise<void> | void
|
|
474
|
+
): void {
|
|
475
|
+
this.taskHandlers.set(taskName, handler);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Check if a task handler is registered for the given task name.
|
|
480
|
+
* @param taskName {string} The task name to check.
|
|
481
|
+
* @returns {boolean} True if a handler is registered.
|
|
482
|
+
*/
|
|
483
|
+
public hasTaskHandler(taskName: string): boolean {
|
|
484
|
+
return this.taskHandlers.has(taskName);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Get a task handler for the given task name.
|
|
489
|
+
* @param taskName {string} The task name.
|
|
490
|
+
* @returns The handler function or undefined if not found.
|
|
491
|
+
*/
|
|
492
|
+
public getTaskHandler(taskName: string):
|
|
493
|
+
| ((
|
|
494
|
+
task: Task,
|
|
495
|
+
data: {
|
|
496
|
+
manifest: Record<string, unknown>;
|
|
497
|
+
downloadPath: string;
|
|
498
|
+
name: string;
|
|
499
|
+
}
|
|
500
|
+
) => Promise<void> | void)
|
|
501
|
+
| undefined {
|
|
502
|
+
return this.taskHandlers.get(taskName);
|
|
503
|
+
}
|
|
504
|
+
|
|
454
505
|
/**
|
|
455
506
|
* Extract a file using 7-Zip on Windows, unzip on Linux/Mac.
|
|
456
507
|
* @param path {string}
|
|
@@ -613,6 +664,68 @@ export class CustomTask {
|
|
|
613
664
|
});
|
|
614
665
|
}
|
|
615
666
|
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* A cleaner API wrapper around EventResponse for task handlers.
|
|
670
|
+
* Provides chainable methods for logging, progress updates, and completion.
|
|
671
|
+
*/
|
|
672
|
+
export class Task {
|
|
673
|
+
private event: EventResponse<void>;
|
|
674
|
+
|
|
675
|
+
constructor(event: EventResponse<void>) {
|
|
676
|
+
this.event = event;
|
|
677
|
+
this.event.defer();
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Log a message to the task. Returns this for chaining.
|
|
682
|
+
* @param message {string} The message to log.
|
|
683
|
+
*/
|
|
684
|
+
log(message: string): this {
|
|
685
|
+
this.event.log(message);
|
|
686
|
+
return this;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Set the progress of the task (0-100). Returns this for chaining.
|
|
691
|
+
* @param progress {number} The progress value (0-100).
|
|
692
|
+
*/
|
|
693
|
+
setProgress(progress: number): this {
|
|
694
|
+
this.event.progress = progress;
|
|
695
|
+
return this;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Complete the task successfully.
|
|
700
|
+
*/
|
|
701
|
+
complete(): void {
|
|
702
|
+
this.event.complete();
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Fail the task with an error message.
|
|
707
|
+
* @param message {string} The error message.
|
|
708
|
+
*/
|
|
709
|
+
fail(message: string): void {
|
|
710
|
+
this.event.fail(message);
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* Ask the user for input using a ConfigurationBuilder screen.
|
|
715
|
+
* The return type is inferred from the ConfigurationBuilder's accumulated option types.
|
|
716
|
+
* @param name {string} The name/title of the input prompt.
|
|
717
|
+
* @param description {string} The description of what input is needed.
|
|
718
|
+
* @param screen {ConfigurationBuilder<U>} The configuration builder for the input form.
|
|
719
|
+
* @returns {Promise<U>} The user's input with types matching the configuration options.
|
|
720
|
+
*/
|
|
721
|
+
async askForInput<U extends Record<string, string | number | boolean>>(
|
|
722
|
+
name: string,
|
|
723
|
+
description: string,
|
|
724
|
+
screen: ConfigurationBuilder<U>
|
|
725
|
+
): Promise<U> {
|
|
726
|
+
return this.event.askForInput(name, description, screen);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
616
729
|
/**
|
|
617
730
|
* A search tool wrapper over Fuse.js for the OGI Addon. This tool is used to search for items in the library.
|
|
618
731
|
* @example
|
|
@@ -762,16 +875,18 @@ class OGIAddonWSListener {
|
|
|
762
875
|
this.registerMessageReceiver();
|
|
763
876
|
}
|
|
764
877
|
|
|
765
|
-
private async userInputAsked
|
|
766
|
-
|
|
878
|
+
private async userInputAsked<
|
|
879
|
+
U extends Record<string, string | number | boolean>,
|
|
880
|
+
>(
|
|
881
|
+
configBuilt: ConfigurationBuilder<U>,
|
|
767
882
|
name: string,
|
|
768
883
|
description: string,
|
|
769
884
|
socket: WebSocket
|
|
770
|
-
): Promise<
|
|
885
|
+
): Promise<U> {
|
|
771
886
|
const config = configBuilt.build(false);
|
|
772
887
|
const id = Math.random().toString(36).substring(7);
|
|
773
888
|
if (!socket) {
|
|
774
|
-
|
|
889
|
+
throw new Error('Socket is not connected');
|
|
775
890
|
}
|
|
776
891
|
socket.send(
|
|
777
892
|
JSON.stringify({
|
|
@@ -784,7 +899,7 @@ class OGIAddonWSListener {
|
|
|
784
899
|
id: id,
|
|
785
900
|
})
|
|
786
901
|
);
|
|
787
|
-
return await this.waitForResponseFromServer(id);
|
|
902
|
+
return await this.waitForResponseFromServer<U>(id);
|
|
788
903
|
}
|
|
789
904
|
|
|
790
905
|
private registerMessageReceiver() {
|
|
@@ -971,19 +1086,62 @@ class OGIAddonWSListener {
|
|
|
971
1086
|
(screen, name, description) =>
|
|
972
1087
|
this.userInputAsked(screen, name, description, this.socket)
|
|
973
1088
|
);
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1089
|
+
|
|
1090
|
+
// Check for taskName: first from args directly (from SearchResult), then from manifest.__taskName (for ActionOption)
|
|
1091
|
+
const taskName =
|
|
1092
|
+
message.args.taskName && typeof message.args.taskName === 'string'
|
|
1093
|
+
? message.args.taskName
|
|
1094
|
+
: message.args.manifest &&
|
|
1095
|
+
typeof message.args.manifest === 'object'
|
|
1096
|
+
? (message.args.manifest as Record<string, unknown>).__taskName
|
|
1097
|
+
: undefined;
|
|
1098
|
+
|
|
1099
|
+
if (
|
|
1100
|
+
taskName &&
|
|
1101
|
+
typeof taskName === 'string' &&
|
|
1102
|
+
this.addon.hasTaskHandler(taskName)
|
|
1103
|
+
) {
|
|
1104
|
+
// Use the registered task handler
|
|
1105
|
+
const handler = this.addon.getTaskHandler(taskName)!;
|
|
1106
|
+
const task = new Task(taskRunEvent);
|
|
1107
|
+
try {
|
|
1108
|
+
const interval = setInterval(() => {
|
|
1109
|
+
if (taskRunEvent.resolved) {
|
|
1110
|
+
clearInterval(interval);
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
this.send('defer-update', {
|
|
1114
|
+
logs: taskRunEvent.logs,
|
|
1115
|
+
deferID: message.args.deferID,
|
|
1116
|
+
progress: taskRunEvent.progress,
|
|
1117
|
+
failed: taskRunEvent.failed,
|
|
1118
|
+
} as ClientSentEventTypes['defer-update']);
|
|
1119
|
+
}, 100);
|
|
1120
|
+
const result = handler(task, {
|
|
1121
|
+
manifest: message.args.manifest || {},
|
|
1122
|
+
downloadPath: message.args.downloadPath || '',
|
|
1123
|
+
name: message.args.name || '',
|
|
1124
|
+
});
|
|
1125
|
+
// If handler returns a promise, wait for it
|
|
1126
|
+
if (result instanceof Promise) {
|
|
1127
|
+
await result;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
977
1130
|
clearInterval(interval);
|
|
978
|
-
|
|
1131
|
+
} catch (error) {
|
|
1132
|
+
taskRunEvent.fail(
|
|
1133
|
+
error instanceof Error ? error.message : String(error)
|
|
1134
|
+
);
|
|
979
1135
|
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1136
|
+
} else {
|
|
1137
|
+
// No handler found - fail the task
|
|
1138
|
+
taskRunEvent.fail(
|
|
1139
|
+
taskName
|
|
1140
|
+
? `No task handler registered for task name: ${taskName}`
|
|
1141
|
+
: 'No task name provided'
|
|
1142
|
+
);
|
|
1143
|
+
}
|
|
1144
|
+
|
|
987
1145
|
const taskRunResult = await this.waitForEventToRespond(taskRunEvent);
|
|
988
1146
|
this.respondToMessage(message.id!!, taskRunResult.data, taskRunEvent);
|
|
989
1147
|
break;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"ConfigurationBuilder-BSuJ4rSI.cjs","names":["z","ZodError"],"sources":["../src/config/ConfigurationBuilder.ts"],"sourcesContent":["import z, { ZodError } from 'zod';\n\nexport interface ConfigurationFile {\n [key: string]: ConfigurationOption;\n}\n\nconst configValidation = z.object({\n name: z.string().min(1),\n displayName: z.string().min(1),\n description: z.string().min(1),\n});\n\nexport function isStringOption(\n option: ConfigurationOption\n): option is StringOption {\n return option.type === 'string';\n}\n\nexport function isNumberOption(\n option: ConfigurationOption\n): option is NumberOption {\n return option.type === 'number';\n}\n\nexport function isBooleanOption(\n option: ConfigurationOption\n): option is BooleanOption {\n return option.type === 'boolean';\n}\n\nexport class ConfigurationBuilder {\n private options: ConfigurationOption[] = [];\n\n /**\n * Add a number option to the configuration builder and return the builder for chaining. You must provide a name, display name, and description for the option.\n * @param option { (option: NumberOption) => NumberOption }\n * @returns\n */\n public addNumberOption(\n option: (option: NumberOption) => NumberOption\n ): ConfigurationBuilder {\n let newOption = new NumberOption();\n newOption = option(newOption);\n this.options.push(newOption);\n return this;\n }\n\n /**\n * Add a string option to the configuration builder and return the builder for chaining. You must provide a name, display name, and description for the option.\n * @param option { (option: StringOption) => StringOption }\n */\n public addStringOption(option: (option: StringOption) => StringOption) {\n let newOption = new StringOption();\n newOption = option(newOption);\n this.options.push(newOption);\n return this;\n }\n\n /**\n * Add a boolean option to the configuration builder and return the builder for chaining. You must provide a name, display name, and description for the option.\n * @param option { (option: BooleanOption) => BooleanOption }\n */\n public addBooleanOption(option: (option: BooleanOption) => BooleanOption) {\n let newOption = new BooleanOption();\n newOption = option(newOption);\n this.options.push(newOption);\n return this;\n }\n\n public build(includeFunctions: boolean): ConfigurationFile {\n let config: ConfigurationFile = {};\n this.options.forEach((option) => {\n // remove all functions from the option object\n if (!includeFunctions) {\n option = JSON.parse(JSON.stringify(option));\n const optionData = configValidation.safeParse(option);\n if (!optionData.success) {\n throw new ZodError(optionData.error.errors);\n }\n\n config[option.name] = option;\n } else {\n config[option.name] = option;\n }\n });\n return config;\n }\n}\n\nexport type ConfigurationOptionType = 'string' | 'number' | 'boolean' | 'unset';\nexport class ConfigurationOption {\n public name: string = '';\n public defaultValue: unknown = '';\n public displayName: string = '';\n public description: string = '';\n public type: ConfigurationOptionType = 'unset';\n\n /**\n * Set the name of the option. **REQUIRED**\n * @param name {string} The name of the option. This is used to reference the option in the configuration file.\n */\n setName(name: string) {\n this.name = name;\n return this;\n }\n\n /**\n * Set the display name of the option. This is used to show the user a human readable version of what the option is. **REQUIRED**\n * @param displayName {string} The display name of the option.\n * @returns\n */\n setDisplayName(displayName: string) {\n this.displayName = displayName;\n return this;\n }\n\n /**\n * Set the description of the option. This is to show the user a brief description of what this option does. **REQUIRED**\n * @param description {string} The description of the option.\n * @returns\n */\n setDescription(description: string) {\n this.description = description;\n return this;\n }\n\n /**\n * Validation code for the option. This is called when the user provides input to the option. If the validation fails, the user will be prompted to provide input again.\n * @param input {unknown} The input to validate\n */\n validate(input: unknown): [boolean, string] {\n throw new Error('Validation code not implemented. Value: ' + input);\n }\n}\n\nexport class StringOption extends ConfigurationOption {\n public allowedValues: string[] = [];\n public minTextLength: number = 0;\n public maxTextLength: number = Number.MAX_SAFE_INTEGER;\n public defaultValue: string = '';\n public inputType: 'text' | 'file' | 'password' | 'folder' = 'text';\n public type: ConfigurationOptionType = 'string';\n\n /**\n * Set the allowed values for the string. If the array is empty, any value is allowed. When provided, the client will act like this option is a dropdown.\n * @param allowedValues {string[]} An array of allowed values for the string. If the array is empty, any value is allowed.\n */\n setAllowedValues(allowedValues: string[]): this {\n this.allowedValues = allowedValues;\n return this;\n }\n\n /**\n * Set the default value for the string. This value will be used if the user does not provide a value. **HIGHLY RECOMMENDED**\n * @param defaultValue {string} The default value for the string.\n */\n setDefaultValue(defaultValue: string): this {\n this.defaultValue = defaultValue;\n return this;\n }\n\n /**\n * Set the minimum text length for the string. If the user provides a string that is less than this value, the validation will fail.\n * @param minTextLength {number} The minimum text length for the string.\n */\n setMinTextLength(minTextLength: number): this {\n this.minTextLength = minTextLength;\n return this;\n }\n\n /**\n * Set the maximum text length for the string. If the user provides a string that is greater than this value, the validation will fail.\n * @param maxTextLength {number} The maximum text length for the string.\n */\n setMaxTextLength(maxTextLength: number): this {\n this.maxTextLength = maxTextLength;\n return this;\n }\n\n /**\n * Set the input type for the string. This will change how the client renders the input.\n * @param inputType {'text' | 'file' | 'password' | 'folder'} The input type for the string.\n */\n setInputType(inputType: 'text' | 'file' | 'password' | 'folder'): this {\n this.inputType = inputType;\n return this;\n }\n\n override validate(input: unknown): [boolean, string] {\n if (typeof input !== 'string') {\n return [false, 'Input is not a string'];\n }\n if (this.allowedValues.length === 0 && input.length !== 0)\n return [true, ''];\n if (\n input.length < this.minTextLength ||\n input.length > this.maxTextLength\n ) {\n return [\n false,\n 'Input is not within the text length ' +\n this.minTextLength +\n ' and ' +\n this.maxTextLength +\n ' characters (currently ' +\n input.length +\n ' characters)',\n ];\n }\n\n return [\n this.allowedValues.includes(input),\n 'Input is not an allowed value',\n ];\n }\n}\n\nexport class NumberOption extends ConfigurationOption {\n public min: number = 0;\n public max: number = Number.MAX_SAFE_INTEGER;\n public defaultValue: number = 0;\n public type: ConfigurationOptionType = 'number';\n public inputType: 'range' | 'number' = 'number';\n\n /**\n * Set the minimum value for the number. If the user provides a number that is less than this value, the validation will fail.\n * @param min {number} The minimum value for the number.\n */\n setMin(min: number): this {\n this.min = min;\n return this;\n }\n\n /**\n * Set the input type for the number. This will change how the client renders the input.\n * @param type {'range' | 'number'} The input type for the number.\n */\n setInputType(type: 'range' | 'number'): this {\n this.inputType = type;\n return this;\n }\n\n /**\n * Set the maximum value for the number. If the user provides a number that is greater than this value, the validation will fail.\n * @param max {number} The maximum value for the number.\n */\n setMax(max: number): this {\n this.max = max;\n return this;\n }\n\n /**\n * Set the default value for the number. This value will be used if the user does not provide a value. **HIGHLY RECOMMENDED**\n * @param defaultValue {number} The default value for the number.\n */\n setDefaultValue(defaultValue: number): this {\n this.defaultValue = defaultValue;\n return this;\n }\n\n override validate(input: unknown): [boolean, string] {\n if (isNaN(Number(input))) {\n return [false, 'Input is not a number'];\n }\n if (Number(input) < this.min || Number(input) > this.max) {\n return [\n false,\n 'Input is not within the range of ' + this.min + ' and ' + this.max,\n ];\n }\n return [true, ''];\n }\n}\n\nexport class BooleanOption extends ConfigurationOption {\n public type: ConfigurationOptionType = 'boolean';\n public defaultValue: boolean = false;\n\n /**\n * Set the default value for the boolean. This value will be used if the user does not provide a value. **HIGHLY RECOMMENDED**\n * @param defaultValue {boolean} The default value for the boolean.\n */\n setDefaultValue(defaultValue: boolean): this {\n this.defaultValue = defaultValue;\n return this;\n }\n\n override validate(input: unknown): [boolean, string] {\n if (typeof input !== 'boolean') {\n return [false, 'Input is not a boolean'];\n }\n return [true, ''];\n }\n}\n"],"mappings":"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"}
|