admin/plugins: online
Massimo Melina committed
May 11, 2022 at 00:11 UTC
7ff290d2c9e6ffb8e5af8938bc44905a185cb99e
14 files changed
+756
-187
admin/src/Form.ts
+10
-4
@@ -18,7 +18,7 @@ import {
18
FormControl,
19
FormControlLabel, FormGroup, FormHelperText,
20
FormLabel,
21
- Grid,
21
+ Grid, InputAdornment,
22
MenuItem, Radio,
23
RadioGroup,
24
Switch,
@@ -201,7 +201,7 @@ export interface FieldProps<T> {
201
[rest: string]: any
202
}
203
204
-export function StringField({ value, onChange, ...props }: FieldProps<string>) {
204
+export function StringField({ value, onChange, typing, start, end, ...props }: FieldProps<string>) {
205
const setter = () => value ?? ''
206
const [state, setState] = useState(setter)
207
@@ -215,7 +215,8 @@ export function StringField({ value, onChange, ...props }: FieldProps<string>) {
215
props.onChange?.(ev)
216
const val = ev.target.value
217
setState(val)
218
- if (document.activeElement !== ev.target) // autofill ongoing, don't wait onBlur event, just go
218
+ if (typing // change state as the user is typing
219
+ || document.activeElement !== ev.target) // autofill ongoing, don't wait onBlur event, just go
220
go(ev, val)
221
},
222
onKeyDown(ev) {
@@ -226,7 +227,12 @@ export function StringField({ value, onChange, ...props }: FieldProps<string>) {
227
onBlur(ev) {
228
props.onBlur?.(ev)
229
go(ev)
229
- }
230
+ },
231
+ InputProps: {
232
+ startAdornment: start && h(InputAdornment, { position: 'start' }, start),
233
+ endAdornment: end && h(InputAdornment, { position: 'end' }, end),
234
+ ...props.InputProps,
235
+ },
236
})
237
238
function go(event: any, val: string=state) {
admin/src/InstalledPlugins.ts
new
+114
@@ -0,0 +1,114 @@
1
+import { apiCall, useApiList } from './api'
2
+import { createElement as h, Fragment } from 'react'
3
+import { Alert, Box, Tooltip } from '@mui/material'
4
+import { DataGrid } from '@mui/x-data-grid'
5
+import { Error, PlayCircle, Settings, StopCircle } from '@mui/icons-material'
6
+import { IconBtn } from './misc'
7
+import { formDialog, toast } from './dialog'
8
+import _ from 'lodash'
9
+import { BoolField, Field, MultiSelectField, NumberField, SelectField, StringField } from './Form'
10
+import { ArrayField } from './ArrayField'
11
+
12
+export default function InstalledPlugins() {
13
+ const { list, error, initializing } = useApiList('get_plugins')
14
+ if (error)
15
+ return h(Alert, { severity: 'error' }, error)
16
+ return h(DataGrid, {
17
+ rows: list,
18
+ loading: initializing,
19
+ disableColumnSelector: true,
20
+ disableColumnMenu: true,
21
+ columns: [
22
+ {
23
+ field: 'id',
24
+ headerName: "name",
25
+ flex: .3,
26
+ minWidth: 150,
27
+ renderCell({ row, value }) {
28
+ return h(Fragment, {},
29
+ value,
30
+ typeof row.badApi === 'string' && h(Tooltip, { title: row.badApi, children: h(Error, { color: 'warning', sx: { ml: 1 } }) })
31
+ )
32
+ }
33
+ },
34
+ {
35
+ field: 'started',
36
+ width: 180,
37
+ valueFormatter: ({ value }) => !value ? "off" : new Date(value as string).toLocaleString()
38
+ },
39
+ {
40
+ field: 'version',
41
+ width: 70,
42
+ },
43
+ {
44
+ field: 'description',
45
+ flex: 1,
46
+ },
47
+ {
48
+ field: "actions",
49
+ width: 80,
50
+ align: 'center',
51
+ hideSortIcons: true,
52
+ disableColumnMenu: true,
53
+ renderCell({ row }) {
54
+ const { config, id } = row
55
+ return h('div', {},
56
+ h(IconBtn, row.started ? {
57
+ icon: StopCircle,
58
+ title: `Stop ${id}`,
59
+ onClick: () =>
60
+ apiCall('set_plugin', { id, enabled: false }).then(() =>
61
+ toast("Plugin is stopping", h(StopCircle, { color: 'warning' })))
62
+ } : {
63
+ icon: PlayCircle,
64
+ title: `Start ${id}`,
65
+ onClick: () =>
66
+ apiCall('set_plugin', { id, enabled: true }).then(() =>
67
+ toast("Plugin is starting", h(PlayCircle, { color: 'success' }))),
68
+ }),
69
+ h(IconBtn, {
70
+ icon: Settings,
71
+ title: "Configuration",
72
+ disabled: !config,
73
+ async onClick() {
74
+ const pl = await apiCall('get_plugin', { id })
75
+ const values = await formDialog({
76
+ title: `${id} configuration`,
77
+ fields: [ h(Box, {}, row.description), ...makeFields(config) ],
78
+ values: pl.config,
79
+ ...row.configDialog,
80
+ })
81
+ if (!values || _.isEqual(pl.config, values)) return
82
+ await apiCall('set_plugin', { id, config: values })
83
+ toast("Configuration saved")
84
+ }
85
+ }),
86
+ )
87
+ }
88
+ },
89
+ ]
90
+ })
91
+}
92
+
93
+function makeFields(config: any) {
94
+ return Object.entries(config).map(([k,o]: [string,any]) => {
95
+ if (!_.isPlainObject(o))
96
+ return o
97
+ let { type, defaultValue, fields, ...rest } = o
98
+ const comp = (type2comp as any)[type] as Field<any> | undefined
99
+ if (comp === ArrayField)
100
+ fields = makeFields(fields)
101
+ if (defaultValue !== undefined && type === 'boolean')
102
+ rest.placeholder = `Default value is ${JSON.stringify(defaultValue)}`
103
+ return { k, comp, fields, ...rest }
104
+ })
105
+}
106
+
107
+const type2comp = {
108
+ string: StringField,
109
+ number: NumberField,
110
+ boolean: BoolField,
111
+ select: SelectField,
112
+ multiselect: MultiSelectField,
113
+ array: ArrayField,
114
+}
admin/src/OnlinePlugins.ts
new
+67
@@ -0,0 +1,67 @@
1
+import { apiCall, useApiList } from './api'
2
+import { Fragment, createElement as h, useState } from 'react'
3
+import { Alert } from '@mui/material'
4
+import { DataGrid } from '@mui/x-data-grid'
5
+import { IconBtn, spinner } from './misc'
6
+import { Download, Search } from '@mui/icons-material'
7
+import { confirmDialog, toast } from './dialog'
8
+import { StringField } from './Form'
9
+import { useDebounce } from 'use-debounce'
10
+
11
+export default function OnlinePlugins() {
12
+ const [search, setSearch] = useState('')
13
+ const [debouncedSearch] = useDebounce(search, 1000)
14
+ const { list, error, initializing } = useApiList('search_online_plugins', { text: debouncedSearch })
15
+ if (error)
16
+ return h(Alert, { severity: 'error' }, error)
17
+ return h(Fragment, {},
18
+ h(StringField, {
19
+ value: search,
20
+ onChange: setSearch as any,
21
+ start: h(Search),
22
+ typing: true,
23
+ label: "Search text"
24
+ }),
25
+ h(DataGrid, {
26
+ rows: list,
27
+ loading: initializing,
28
+ columns: [
29
+ {
30
+ field: 'id',
31
+ headerName: "name",
32
+ flex: .3,
33
+ minWidth: 150,
34
+ },
35
+ {
36
+ field: 'version',
37
+ width: 70,
38
+ },
39
+ {
40
+ field: 'description',
41
+ flex: 1,
42
+ },
43
+ {
44
+ field: "actions",
45
+ width: 80,
46
+ align: 'center',
47
+ hideSortIcons: true,
48
+ disableColumnMenu: true,
49
+ renderCell({ row }) {
50
+ return row.downloading ? spinner() : h(IconBtn, {
51
+ icon: Download,
52
+ title: "Download",
53
+ disabled: row.installed && "Already installed",
54
+ async onClick() {
55
+ if (!await confirmDialog("WARNING - Proceed only if you trust this author and this plugin")) return
56
+ const { id } = row
57
+ return apiCall('download_plugin', { id })
58
+ .then(() => toast("Plugin downloaded: " + id))
59
+ }
60
+ })
61
+ }
62
+ },
63
+ ]
64
+ })
65
+ )
66
+}
67
+
admin/src/PluginsPage.ts
+11
-111
@@ -1,116 +1,16 @@
1
// This file is part of HFS - Copyright 2021-2022, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3
-import { createElement as h, Fragment } from "react"
4
-import { apiCall, useApiList } from './api'
5
-import { DataGrid } from '@mui/x-data-grid'
6
-import { Alert, Box, Tooltip } from '@mui/material'
7
-import { IconBtn } from './misc'
8
-import { Error, PlayCircle, Settings, StopCircle } from '@mui/icons-material'
9
-import { toast, formDialog } from './dialog'
10
-import { BoolField, Field, MultiSelectField, NumberField, SelectField, StringField } from './Form'
11
-import { ArrayField } from './ArrayField'
12
-import _ from "lodash"
3
+import { createElement as h, Fragment, useState } from "react"
4
+import { Tab, Tabs } from '@mui/material'
5
+import InstalledPlugins from "./InstalledPlugins"
6
+import OnlinePlugins from "./OnlinePlugins"
7
8
export default function PluginsPage() {
15
- const { list, error, initializing } = useApiList('get_plugins')
16
- if (error)
17
- return h(Alert, { severity: 'error' }, error)
18
- return h(DataGrid, {
19
- rows: list,
20
- loading: initializing,
21
- disableColumnSelector: true,
22
- disableColumnMenu: true,
23
- columns: [
24
- {
25
- field: 'id',
26
- headerName: "name",
27
- flex: .3,
28
- minWidth: 150,
29
- renderCell({ row, value }) {
30
- return h(Fragment, {},
31
- value,
32
- typeof row.badApi === 'string' && h(Tooltip, { title: row.badApi, children: h(Error, { color: 'warning', sx: { ml: 1 } }) })
33
- )
34
- }
35
- },
36
- {
37
- field: 'started',
38
- width: 180,
39
- valueFormatter: ({ value }) => !value ? "off" : new Date(value as string).toLocaleString()
40
- },
41
- {
42
- field: 'version',
43
- width: 70,
44
- },
45
- {
46
- field: 'description',
47
- flex: 1,
48
- },
49
- {
50
- field: "actions",
51
- width: 80,
52
- align: 'center',
53
- hideSortIcons: true,
54
- disableColumnMenu: true,
55
- renderCell({ row }) {
56
- const { config, id } = row
57
- return h('div', {},
58
- h(IconBtn, row.started ? {
59
- icon: StopCircle,
60
- title: `Stop ${id}`,
61
- onClick: () =>
62
- apiCall('set_plugin', { id, enabled: false }).then(() =>
63
- toast("Plugin is stopping", h(StopCircle, { color: 'warning' })))
64
- } : {
65
- icon: PlayCircle,
66
- title: `Start ${id}`,
67
- onClick: () =>
68
- apiCall('set_plugin', { id, enabled: true }).then(() =>
69
- toast("Plugin is starting", h(PlayCircle, { color: 'success' }))),
70
- }),
71
- h(IconBtn, {
72
- icon: Settings,
73
- title: "Configuration",
74
- disabled: !config,
75
- async onClick() {
76
- const pl = await apiCall('get_plugin', { id })
77
- const values = await formDialog({
78
- title: `${id} configuration`,
79
- fields: [ h(Box, {}, row.description), ...makeFields(config) ],
80
- values: pl.config,
81
- ...row.configDialog,
82
- })
83
- if (!values || _.isEqual(pl.config, values)) return
84
- await apiCall('set_plugin', { id, config: values })
85
- toast("Configuration saved")
86
- }
87
- }),
88
- )
89
- }
90
- },
91
- ]
92
- })
93
-}
94
-
95
-function makeFields(config: any) {
96
- return Object.entries(config).map(([k,o]: [string,any]) => {
97
- if (!_.isPlainObject(o))
98
- return o
99
- let { type, defaultValue, fields, ...rest } = o
100
- const comp = (type2comp as any)[type] as Field<any> | undefined
101
- if (comp === ArrayField)
102
- fields = makeFields(fields)
103
- if (defaultValue !== undefined && type === 'boolean')
104
- rest.placeholder = `Default value is ${JSON.stringify(defaultValue)}`
105
- return { k, comp, fields, ...rest }
106
- })
107
-}
108
-
109
-const type2comp = {
110
- string: StringField,
111
- number: NumberField,
112
- boolean: BoolField,
113
- select: SelectField,
114
- multiselect: MultiSelectField,
115
- array: ArrayField,
9
+ const [tab, setTab] = useState(0)
10
+ const tabs = ["Installed", "Search online"]
11
+ return h(Fragment, {},
12
+ h(Tabs, { value: tab, onChange(ev,i){ setTab(i) } },
13
+ tabs.map(f => h(Tab, { label: f, key: f })) ),
14
+ h(tab ? OnlinePlugins : InstalledPlugins)
15
+ )
16
}
admin/src/api.ts
+2
-1
@@ -120,6 +120,7 @@ export function useApiList<T=any>(cmd:string|Falsy, params: Dict={}, { addId=fal
120
setList(list => [ ...list, ...chunk ])
121
}
122
setLoading(true)
123
+ setInitializing(true)
124
setList([])
125
const timer = setInterval(flush, 1000)
126
const src = apiEvents(cmd, params, (type, data) => {
@@ -133,7 +134,7 @@ export function useApiList<T=any>(cmd:string|Falsy, params: Dict={}, { addId=fal
134
case 'msg':
135
if (src?.readyState === src?.CLOSED)
136
return stop()
136
- if (data === 'init') {
137
+ if (data === 'init' || data === 'end') {
138
flush()
139
setInitializing(false)
140
return
admin/src/misc.ts
+5
-2
@@ -28,10 +28,13 @@ export function modifiedSx(is: boolean) {
28
return is ? { outline: '2px solid' } : undefined
29
}
30
31
-export function IconBtn({ title, icon, onClick, ...rest }: { title?: string, icon: SvgIconComponent, [rest:string]:any }) {
31
+interface IconBtnProps { title?: string, icon: SvgIconComponent, disabled?: boolean | string, [rest:string]:any }
32
+export function IconBtn({ title, icon, onClick, disabled, ...rest }: IconBtnProps) {
33
const [loading, setLoading] = useStateMounted(false)
34
+ if (typeof disabled === 'string')
35
+ title = disabled
36
let ret: ReturnType<FC> = h(IconButton, {
34
- disabled: loading,
37
+ disabled: loading || Boolean(disabled),
38
...rest,
39
onClick() {
40
const ret = onClick?.apply(this,arguments)
dev-plugins.md
+10
@@ -131,3 +131,13 @@ This is a list of available frontend events, with respective parameters and outp
131
- `m?: Date` modified-time.
132
- output `string | void`
133
134
+## Publish your plug-in
135
+
136
+Suggested method for publishing is to have a dedicated repository on github, with topic `hfs-plugin`.
137
+To set the topic go on the repo home and click on the gear icon near the "About" box.
138
+Be sure to also fill the "description" field, especially with words that people may search for.
139
+
140
+The files intended to be installed must go in a folder named `dist`.
141
+You can keep other files outside.
142
+
143
+You can refer to this dummy plugin for reference https://github.com/rejetto/demo-plugin .
package-lock.json
+349
-47
@@ -5180,6 +5180,15 @@
5180
"integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==",
5181
"dev": true
5182
},
5183
+ "node_modules/@types/unzipper": {
5184
+ "version": "0.10.5",
5185
+ "resolved": "https://registry.npmjs.org/@types/unzipper/-/unzipper-0.10.5.tgz",
5186
+ "integrity": "sha512-NrLJb29AdnBARpg9S/4ktfPEisbJ0AvaaAr3j7Q1tg8AgcEUsq2HqbNzvgLRoWyRtjzeLEv7vuL39u1mrNIyNA==",
5187
+ "dev": true,
5188
+ "dependencies": {
5189
+ "@types/node": "*"
5190
+ }
5191
+ },
5192
"node_modules/@types/ws": {
5193
"version": "8.5.3",
5194
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
@@ -6463,8 +6472,7 @@
6472
"node_modules/balanced-match": {
6473
"version": "1.0.2",
6474
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
6466
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
6467
- "dev": true
6475
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
6476
},
6477
"node_modules/base64-js": {
6478
"version": "1.5.1",
@@ -6506,6 +6514,14 @@
6514
"node": ">= 8.0.0"
6515
}
6516
},
6517
+ "node_modules/big-integer": {
6518
+ "version": "1.6.51",
6519
+ "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz",
6520
+ "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg==",
6521
+ "engines": {
6522
+ "node": ">=0.6"
6523
+ }
6524
+ },
6525
"node_modules/big.js": {
6526
"version": "5.2.2",
6527
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
@@ -6515,6 +6531,18 @@
6531
"node": "*"
6532
}
6533
},
6534
+ "node_modules/binary": {
6535
+ "version": "0.3.0",
6536
+ "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz",
6537
+ "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=",
6538
+ "dependencies": {
6539
+ "buffers": "~0.1.1",
6540
+ "chainsaw": "~0.1.0"
6541
+ },
6542
+ "engines": {
6543
+ "node": "*"
6544
+ }
6545
+ },
6546
"node_modules/binary-extensions": {
6547
"version": "2.2.0",
6548
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@@ -6741,7 +6769,6 @@
6769
"version": "1.1.11",
6770
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
6771
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
6744
- "dev": true,
6772
"dependencies": {
6773
"balanced-match": "^1.0.0",
6774
"concat-map": "0.0.1"
@@ -6844,6 +6871,22 @@
6871
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
6872
"dev": true
6873
},
6874
+ "node_modules/buffer-indexof-polyfill": {
6875
+ "version": "1.0.2",
6876
+ "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz",
6877
+ "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A==",
6878
+ "engines": {
6879
+ "node": ">=0.10"
6880
+ }
6881
+ },
6882
+ "node_modules/buffers": {
6883
+ "version": "0.1.1",
6884
+ "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz",
6885
+ "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s=",
6886
+ "engines": {
6887
+ "node": ">=0.2.0"
6888
+ }
6889
+ },
6890
"node_modules/builtin-modules": {
6891
"version": "3.2.0",
6892
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz",
@@ -7029,6 +7072,17 @@
7072
"node": ">=4"
7073
}
7074
},
7075
+ "node_modules/chainsaw": {
7076
+ "version": "0.1.0",
7077
+ "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz",
7078
+ "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=",
7079
+ "dependencies": {
7080
+ "traverse": ">=0.3.0 <0.4"
7081
+ },
7082
+ "engines": {
7083
+ "node": "*"
7084
+ }
7085
+ },
7086
"node_modules/chalk": {
7087
"version": "2.4.2",
7088
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
@@ -7371,8 +7425,7 @@
7425
"node_modules/concat-map": {
7426
"version": "0.0.1",
7427
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
7374
- "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
7375
- "dev": true
7428
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
7429
},
7430
"node_modules/configstore": {
7431
"version": "5.0.1",
@@ -8581,6 +8634,36 @@
8634
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
8635
"dev": true
8636
},
8637
+ "node_modules/duplexer2": {
8638
+ "version": "0.1.4",
8639
+ "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
8640
+ "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=",
8641
+ "dependencies": {
8642
+ "readable-stream": "^2.0.2"
8643
+ }
8644
+ },
8645
+ "node_modules/duplexer2/node_modules/readable-stream": {
8646
+ "version": "2.3.7",
8647
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
8648
+ "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
8649
+ "dependencies": {
8650
+ "core-util-is": "~1.0.0",
8651
+ "inherits": "~2.0.3",
8652
+ "isarray": "~1.0.0",
8653
+ "process-nextick-args": "~2.0.0",
8654
+ "safe-buffer": "~5.1.1",
8655
+ "string_decoder": "~1.1.1",
8656
+ "util-deprecate": "~1.0.1"
8657
+ }
8658
+ },
8659
+ "node_modules/duplexer2/node_modules/string_decoder": {
8660
+ "version": "1.1.1",
8661
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
8662
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
8663
+ "dependencies": {
8664
+ "safe-buffer": "~5.1.0"
8665
+ }
8666
+ },
8667
"node_modules/duplexer3": {
8668
"version": "0.1.4",
8669
"resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
@@ -10425,8 +10508,7 @@
10508
"node_modules/fs.realpath": {
10509
"version": "1.0.0",
10510
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
10428
- "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
10429
- "dev": true
10511
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
10512
},
10513
"node_modules/fsevents": {
10514
"version": "2.3.2",
@@ -10442,6 +10524,31 @@
10524
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
10525
}
10526
},
10527
+ "node_modules/fstream": {
10528
+ "version": "1.0.12",
10529
+ "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz",
10530
+ "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==",
10531
+ "dependencies": {
10532
+ "graceful-fs": "^4.1.2",
10533
+ "inherits": "~2.0.0",
10534
+ "mkdirp": ">=0.5 0",
10535
+ "rimraf": "2"
10536
+ },
10537
+ "engines": {
10538
+ "node": ">=0.6"
10539
+ }
10540
+ },
10541
+ "node_modules/fstream/node_modules/rimraf": {
10542
+ "version": "2.7.1",
10543
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
10544
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
10545
+ "dependencies": {
10546
+ "glob": "^7.1.3"
10547
+ },
10548
+ "bin": {
10549
+ "rimraf": "bin.js"
10550
+ }
10551
+ },
10552
"node_modules/function-bind": {
10553
"version": "1.1.1",
10554
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@@ -10540,7 +10647,6 @@
10647
"version": "7.2.0",
10648
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
10649
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
10543
- "dev": true,
10650
"dependencies": {
10651
"fs.realpath": "^1.0.0",
10652
"inflight": "^1.0.4",
@@ -10577,7 +10683,6 @@
10683
"version": "3.1.2",
10684
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
10685
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
10580
- "dev": true,
10686
"dependencies": {
10687
"brace-expansion": "^1.1.7"
10688
},
@@ -10712,8 +10817,7 @@
10817
"node_modules/graceful-fs": {
10818
"version": "4.2.10",
10819
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
10715
- "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==",
10716
- "dev": true
10820
+ "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="
10821
},
10822
"node_modules/growl": {
10823
"version": "1.10.5",
@@ -11309,7 +11413,6 @@
11413
"version": "1.0.6",
11414
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
11415
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
11312
- "dev": true,
11416
"dependencies": {
11417
"once": "^1.3.0",
11418
"wrappy": "1"
@@ -11846,8 +11949,7 @@
11949
"node_modules/isarray": {
11950
"version": "1.0.0",
11951
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
11849
- "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
11850
- "dev": true
11952
+ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
11953
},
11954
"node_modules/isexe": {
11955
"version": "2.0.0",
@@ -14600,6 +14702,11 @@
14702
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
14703
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
14704
},
14705
+ "node_modules/listenercount": {
14706
+ "version": "1.0.1",
14707
+ "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz",
14708
+ "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc="
14709
+ },
14710
"node_modules/loader-runner": {
14711
"version": "4.3.0",
14712
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
@@ -15150,7 +15257,6 @@
15257
"version": "0.5.6",
15258
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
15259
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
15153
- "dev": true,
15260
"dependencies": {
15261
"minimist": "^1.2.6"
15262
},
@@ -15679,7 +15785,6 @@
15785
"version": "1.4.0",
15786
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
15787
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
15682
- "dev": true,
15788
"dependencies": {
15789
"wrappy": "1"
15790
}
@@ -15888,7 +15993,6 @@
15993
"version": "1.0.1",
15994
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
15995
"integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
15891
- "dev": true,
15996
"engines": {
15997
"node": ">=0.10.0"
15998
}
@@ -17371,8 +17475,7 @@
17475
"node_modules/process-nextick-args": {
17476
"version": "2.0.1",
17477
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
17374
- "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
17375
- "dev": true
17478
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
17479
},
17480
"node_modules/promise": {
17481
"version": "8.1.0",
@@ -18991,6 +19094,11 @@
19094
"node": ">= 0.8.0"
19095
}
19096
},
19097
+ "node_modules/setimmediate": {
19098
+ "version": "1.0.5",
19099
+ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
19100
+ "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
19101
+ },
19102
"node_modules/setprototypeof": {
19103
"version": "1.2.0",
19104
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -20041,6 +20149,14 @@
20149
"node": ">=8"
20150
}
20151
},
20152
+ "node_modules/traverse": {
20153
+ "version": "0.3.9",
20154
+ "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
20155
+ "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk=",
20156
+ "engines": {
20157
+ "node": "*"
20158
+ }
20159
+ },
20160
"node_modules/trim-newlines": {
20161
"version": "3.0.1",
20162
"resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz",
@@ -20360,6 +20476,50 @@
20476
"integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=",
20477
"dev": true
20478
},
20479
+ "node_modules/unzipper": {
20480
+ "version": "0.10.11",
20481
+ "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz",
20482
+ "integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==",
20483
+ "dependencies": {
20484
+ "big-integer": "^1.6.17",
20485
+ "binary": "~0.3.0",
20486
+ "bluebird": "~3.4.1",
20487
+ "buffer-indexof-polyfill": "~1.0.0",
20488
+ "duplexer2": "~0.1.4",
20489
+ "fstream": "^1.0.12",
20490
+ "graceful-fs": "^4.2.2",
20491
+ "listenercount": "~1.0.1",
20492
+ "readable-stream": "~2.3.6",
20493
+ "setimmediate": "~1.0.4"
20494
+ }
20495
+ },
20496
+ "node_modules/unzipper/node_modules/bluebird": {
20497
+ "version": "3.4.7",
20498
+ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
20499
+ "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM="
20500
+ },
20501
+ "node_modules/unzipper/node_modules/readable-stream": {
20502
+ "version": "2.3.7",
20503
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
20504
+ "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
20505
+ "dependencies": {
20506
+ "core-util-is": "~1.0.0",
20507
+ "inherits": "~2.0.3",
20508
+ "isarray": "~1.0.0",
20509
+ "process-nextick-args": "~2.0.0",
20510
+ "safe-buffer": "~5.1.1",
20511
+ "string_decoder": "~1.1.1",
20512
+ "util-deprecate": "~1.0.1"
20513
+ }
20514
+ },
20515
+ "node_modules/unzipper/node_modules/string_decoder": {
20516
+ "version": "1.1.1",
20517
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
20518
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
20519
+ "dependencies": {
20520
+ "safe-buffer": "~5.1.0"
20521
+ }
20522
+ },
20523
"node_modules/upath": {
20524
"version": "1.2.0",
20525
"resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz",
@@ -20526,8 +20686,7 @@
20686
"node_modules/util-deprecate": {
20687
"version": "1.0.2",
20688
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
20529
- "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
20530
- "dev": true
20689
+ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
20690
},
20691
"node_modules/util.promisify": {
20692
"version": "1.0.1",
@@ -21615,8 +21774,7 @@
21774
"node_modules/wrappy": {
21775
"version": "1.0.2",
21776
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
21618
- "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
21619
- "dev": true
21777
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
21778
},
21779
"node_modules/write-file-atomic": {
21780
"version": "3.0.3",
@@ -21826,6 +21984,7 @@
21984
"minimist": "^1.2.6",
21985
"open": "^8.4.0",
21986
"tssrp6a": "^3.0.0",
21987
+ "unzipper": "^0.10.11",
21988
"yaml": "^2.0.0-10"
21989
},
21990
"bin": {
@@ -21844,6 +22003,7 @@
22003
"@types/minimist": "^1.2.2",
22004
"@types/mocha": "^9.0.0",
22005
"@types/node": "^16.11.12",
22006
+ "@types/unzipper": "^0.10.5",
22007
"axios": "^0.24.0",
22008
"koa-better-http-proxy": "^0.2.9",
22009
"mocha": "^9.1.3",
@@ -23523,6 +23683,7 @@
23683
"@types/minimist": "^1.2.2",
23684
"@types/mocha": "^9.0.0",
23685
"@types/node": "^16.11.12",
23686
+ "@types/unzipper": "^0.10.5",
23687
"axios": "^0.24.0",
23688
"buffer-crc32": "https://github.com/rejetto/buffer-crc32.git",
23689
"cidr-tools": "^4.3.0",
@@ -23542,6 +23703,7 @@
23703
"open": "^8.4.0",
23704
"ts-node": "^10.4.0",
23705
"tssrp6a": "^3.0.0",
23706
+ "unzipper": "^0.10.11",
23707
"yaml": "^2.0.0-10"
23708
}
23709
},
@@ -25517,6 +25679,15 @@
25679
"integrity": "sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg==",
25680
"dev": true
25681
},
25682
+ "@types/unzipper": {
25683
+ "version": "0.10.5",
25684
+ "resolved": "https://registry.npmjs.org/@types/unzipper/-/unzipper-0.10.5.tgz",
25685
+ "integrity": "sha512-NrLJb29AdnBARpg9S/4ktfPEisbJ0AvaaAr3j7Q1tg8AgcEUsq2HqbNzvgLRoWyRtjzeLEv7vuL39u1mrNIyNA==",
25686
+ "dev": true,
25687
+ "requires": {
25688
+ "@types/node": "*"
25689
+ }
25690
+ },
25691
"@types/ws": {
25692
"version": "8.5.3",
25693
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz",
@@ -26479,8 +26650,7 @@
26650
"balanced-match": {
26651
"version": "1.0.2",
26652
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
26482
- "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
26483
- "dev": true
26653
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="
26654
},
26655
"base64-js": {
26656
"version": "1.5.1",
@@ -26505,12 +26675,26 @@
26675
"tryer": "^1.0.1"
26676
}
26677
},
26678
+ "big-integer": {
26679
+ "version": "1.6.51",
26680
+ "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.51.tgz",
26681
+ "integrity": "sha512-GPEid2Y9QU1Exl1rpO9B2IPJGHPSupF5GnVIP0blYvNOMer2bTvSWs1jGOUg04hTmu67nmLsQ9TBo1puaotBHg=="
26682
+ },
26683
"big.js": {
26684
"version": "5.2.2",
26685
"resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz",
26686
"integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==",
26687
"dev": true
26688
},
26689
+ "binary": {
26690
+ "version": "0.3.0",
26691
+ "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz",
26692
+ "integrity": "sha1-n2BVO8XOjDOG87VTz/R0Yq3sqnk=",
26693
+ "requires": {
26694
+ "buffers": "~0.1.1",
26695
+ "chainsaw": "~0.1.0"
26696
+ }
26697
+ },
26698
"binary-extensions": {
26699
"version": "2.2.0",
26700
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@@ -26686,7 +26870,6 @@
26870
"version": "1.1.11",
26871
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
26872
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
26689
- "dev": true,
26873
"requires": {
26874
"balanced-match": "^1.0.0",
26875
"concat-map": "0.0.1"
@@ -26752,6 +26935,16 @@
26935
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
26936
"dev": true
26937
},
26938
+ "buffer-indexof-polyfill": {
26939
+ "version": "1.0.2",
26940
+ "resolved": "https://registry.npmjs.org/buffer-indexof-polyfill/-/buffer-indexof-polyfill-1.0.2.tgz",
26941
+ "integrity": "sha512-I7wzHwA3t1/lwXQh+A5PbNvJxgfo5r3xulgpYDB5zckTu/Z9oUK9biouBKQUjEqzaz3HnAT6TYoovmE+GqSf7A=="
26942
+ },
26943
+ "buffers": {
26944
+ "version": "0.1.1",
26945
+ "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz",
26946
+ "integrity": "sha1-skV5w77U1tOWru5tmorn9Ugqt7s="
26947
+ },
26948
"builtin-modules": {
26949
"version": "3.2.0",
26950
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.2.0.tgz",
@@ -26881,6 +27074,14 @@
27074
"integrity": "sha512-roIFONhcxog0JSSWbvVAh3OocukmSgpqOH6YpMkCvav/ySIV3JKg4Dc8vYtQjYi/UxpNE36r/9v+VqTQqgkYmw==",
27075
"dev": true
27076
},
27077
+ "chainsaw": {
27078
+ "version": "0.1.0",
27079
+ "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz",
27080
+ "integrity": "sha1-XqtQsor+WAdNDVgpE4iCi15fvJg=",
27081
+ "requires": {
27082
+ "traverse": ">=0.3.0 <0.4"
27083
+ }
27084
+ },
27085
"chalk": {
27086
"version": "2.4.2",
27087
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
@@ -27154,8 +27355,7 @@
27355
"concat-map": {
27356
"version": "0.0.1",
27357
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
27157
- "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
27158
- "dev": true
27358
+ "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
27359
},
27360
"configstore": {
27361
"version": "5.0.1",
@@ -28053,6 +28253,38 @@
28253
"integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==",
28254
"dev": true
28255
},
28256
+ "duplexer2": {
28257
+ "version": "0.1.4",
28258
+ "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz",
28259
+ "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=",
28260
+ "requires": {
28261
+ "readable-stream": "^2.0.2"
28262
+ },
28263
+ "dependencies": {
28264
+ "readable-stream": {
28265
+ "version": "2.3.7",
28266
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
28267
+ "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
28268
+ "requires": {
28269
+ "core-util-is": "~1.0.0",
28270
+ "inherits": "~2.0.3",
28271
+ "isarray": "~1.0.0",
28272
+ "process-nextick-args": "~2.0.0",
28273
+ "safe-buffer": "~5.1.1",
28274
+ "string_decoder": "~1.1.1",
28275
+ "util-deprecate": "~1.0.1"
28276
+ }
28277
+ },
28278
+ "string_decoder": {
28279
+ "version": "1.1.1",
28280
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
28281
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
28282
+ "requires": {
28283
+ "safe-buffer": "~5.1.0"
28284
+ }
28285
+ }
28286
+ }
28287
+ },
28288
"duplexer3": {
28289
"version": "0.1.4",
28290
"resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz",
@@ -29429,8 +29661,7 @@
29661
"fs.realpath": {
29662
"version": "1.0.0",
29663
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
29432
- "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=",
29433
- "dev": true
29664
+ "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8="
29665
},
29666
"fsevents": {
29667
"version": "2.3.2",
@@ -29439,6 +29670,27 @@
29670
"dev": true,
29671
"optional": true
29672
},
29673
+ "fstream": {
29674
+ "version": "1.0.12",
29675
+ "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz",
29676
+ "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==",
29677
+ "requires": {
29678
+ "graceful-fs": "^4.1.2",
29679
+ "inherits": "~2.0.0",
29680
+ "mkdirp": ">=0.5 0",
29681
+ "rimraf": "2"
29682
+ },
29683
+ "dependencies": {
29684
+ "rimraf": {
29685
+ "version": "2.7.1",
29686
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz",
29687
+ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==",
29688
+ "requires": {
29689
+ "glob": "^7.1.3"
29690
+ }
29691
+ }
29692
+ }
29693
+ },
29694
"function-bind": {
29695
"version": "1.1.1",
29696
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
@@ -29510,7 +29762,6 @@
29762
"version": "7.2.0",
29763
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz",
29764
"integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==",
29513
- "dev": true,
29765
"requires": {
29766
"fs.realpath": "^1.0.0",
29767
"inflight": "^1.0.4",
@@ -29524,7 +29775,6 @@
29775
"version": "3.1.2",
29776
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
29777
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
29527
- "dev": true,
29778
"requires": {
29779
"brace-expansion": "^1.1.7"
29780
}
@@ -29645,8 +29895,7 @@
29895
"graceful-fs": {
29896
"version": "4.2.10",
29897
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
29648
- "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==",
29649
- "dev": true
29898
+ "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="
29899
},
29900
"growl": {
29901
"version": "1.10.5",
@@ -30082,7 +30331,6 @@
30331
"version": "1.0.6",
30332
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
30333
"integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=",
30085
- "dev": true,
30334
"requires": {
30335
"once": "^1.3.0",
30336
"wrappy": "1"
@@ -30461,8 +30709,7 @@
30709
"isarray": {
30710
"version": "1.0.0",
30711
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
30464
- "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=",
30465
- "dev": true
30712
+ "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE="
30713
},
30714
"isexe": {
30715
"version": "2.0.0",
@@ -32571,6 +32818,11 @@
32818
"resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz",
32819
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="
32820
},
32821
+ "listenercount": {
32822
+ "version": "1.0.1",
32823
+ "resolved": "https://registry.npmjs.org/listenercount/-/listenercount-1.0.1.tgz",
32824
+ "integrity": "sha1-hMinKrWcRyUyFIDJdeZQg0LnCTc="
32825
+ },
32826
"loader-runner": {
32827
"version": "4.3.0",
32828
"resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz",
@@ -32990,7 +33242,6 @@
33242
"version": "0.5.6",
33243
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
33244
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
32993
- "dev": true,
33245
"requires": {
33246
"minimist": "^1.2.6"
33247
}
@@ -33377,7 +33628,6 @@
33628
"version": "1.4.0",
33629
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
33630
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
33380
- "dev": true,
33631
"requires": {
33632
"wrappy": "1"
33633
}
@@ -33531,8 +33781,7 @@
33781
"path-is-absolute": {
33782
"version": "1.0.1",
33783
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
33534
- "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=",
33535
- "dev": true
33784
+ "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18="
33785
},
33786
"path-key": {
33787
"version": "3.1.1",
@@ -34493,8 +34742,7 @@
34742
"process-nextick-args": {
34743
"version": "2.0.1",
34744
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
34496
- "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
34497
- "dev": true
34745
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
34746
},
34747
"promise": {
34748
"version": "8.1.0",
@@ -35724,6 +35972,11 @@
35972
"send": "0.18.0"
35973
}
35974
},
35975
+ "setimmediate": {
35976
+ "version": "1.0.5",
35977
+ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
35978
+ "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
35979
+ },
35980
"setprototypeof": {
35981
"version": "1.2.0",
35982
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
@@ -36543,6 +36796,11 @@
36796
"punycode": "^2.1.1"
36797
}
36798
},
36799
+ "traverse": {
36800
+ "version": "0.3.9",
36801
+ "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz",
36802
+ "integrity": "sha1-cXuPIgzAu3tE5AUUwisui7xw2Lk="
36803
+ },
36804
"trim-newlines": {
36805
"version": "3.0.1",
36806
"resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz",
@@ -36776,6 +37034,52 @@
37034
"integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=",
37035
"dev": true
37036
},
37037
+ "unzipper": {
37038
+ "version": "0.10.11",
37039
+ "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.10.11.tgz",
37040
+ "integrity": "sha512-+BrAq2oFqWod5IESRjL3S8baohbevGcVA+teAIOYWM3pDVdseogqbzhhvvmiyQrUNKFUnDMtELW3X8ykbyDCJw==",
37041
+ "requires": {
37042
+ "big-integer": "^1.6.17",
37043
+ "binary": "~0.3.0",
37044
+ "bluebird": "~3.4.1",
37045
+ "buffer-indexof-polyfill": "~1.0.0",
37046
+ "duplexer2": "~0.1.4",
37047
+ "fstream": "^1.0.12",
37048
+ "graceful-fs": "^4.2.2",
37049
+ "listenercount": "~1.0.1",
37050
+ "readable-stream": "~2.3.6",
37051
+ "setimmediate": "~1.0.4"
37052
+ },
37053
+ "dependencies": {
37054
+ "bluebird": {
37055
+ "version": "3.4.7",
37056
+ "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz",
37057
+ "integrity": "sha1-9y12C+Cbf3bQjtj66Ysomo0F+rM="
37058
+ },
37059
+ "readable-stream": {
37060
+ "version": "2.3.7",
37061
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
37062
+ "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
37063
+ "requires": {
37064
+ "core-util-is": "~1.0.0",
37065
+ "inherits": "~2.0.3",
37066
+ "isarray": "~1.0.0",
37067
+ "process-nextick-args": "~2.0.0",
37068
+ "safe-buffer": "~5.1.1",
37069
+ "string_decoder": "~1.1.1",
37070
+ "util-deprecate": "~1.0.1"
37071
+ }
37072
+ },
37073
+ "string_decoder": {
37074
+ "version": "1.1.1",
37075
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
37076
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
37077
+ "requires": {
37078
+ "safe-buffer": "~5.1.0"
37079
+ }
37080
+ }
37081
+ }
37082
+ },
37083
"upath": {
37084
"version": "1.2.0",
37085
"resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz",
@@ -36897,8 +37201,7 @@
37201
"util-deprecate": {
37202
"version": "1.0.2",
37203
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
36900
- "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=",
36901
- "dev": true
37204
+ "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8="
37205
},
37206
"util.promisify": {
37207
"version": "1.0.1",
@@ -37756,8 +38059,7 @@
38059
"wrappy": {
38060
"version": "1.0.2",
38061
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
37759
- "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
37760
- "dev": true
38062
+ "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
38063
},
38064
"write-file-atomic": {
38065
"version": "3.0.3",
server/package.json
+3
-1
@@ -25,7 +25,8 @@
25
"minimist": "^1.2.6",
26
"open": "^8.4.0",
27
"tssrp6a": "^3.0.0",
28
- "yaml": "^2.0.0-10"
28
+ "yaml": "^2.0.0-10",
29
+ "unzipper": "^0.10.11"
30
},
31
"devDependencies": {
32
"@types/archiver": "^5.1.1",
@@ -40,6 +41,7 @@
41
"@types/minimist": "^1.2.2",
42
"@types/mocha": "^9.0.0",
43
"@types/node": "^16.11.12",
44
+ "@types/unzipper": "^0.10.5",
45
"axios": "^0.24.0",
46
"koa-better-http-proxy": "^0.2.9",
47
"mocha": "^9.1.3",
server/src/api.plugins.ts
+110
-3
@@ -4,12 +4,20 @@ import {
4
getAvailablePlugins,
5
getPluginConfigFields,
6
mapPlugins,
7
- Plugin, pluginsConfig
7
+ parsePluginSource,
8
+ Plugin, pluginsConfig,
9
+ PATH as PLUGINS_PATH
10
} from './plugins'
11
import _ from 'lodash'
12
import assert from 'assert'
11
-import { objSameKeys, same } from './misc'
12
-import { ApiHandlers, sendList } from './apiMiddleware'
13
+import { httpsString, httpsStream, objSameKeys, onOff, same } from './misc'
14
+import { ApiError, ApiHandlers, sendList } from './apiMiddleware'
15
+import events from './events'
16
+import unzipper from 'unzipper'
17
+import { createWriteStream } from 'fs'
18
+import { access, mkdir } from 'fs/promises'
19
+
20
+const DIST_ROOT = 'dist/'
21
22
const apis: ApiHandlers = {
23
get_plugins({}, ctx) {
@@ -57,6 +65,105 @@ const apis: ApiHandlers = {
65
}
66
},
67
68
+ search_online_plugins({ text }, ctx) {
69
+ const list = sendList()
70
+ const repo2id = Object.fromEntries(getAvailablePlugins().map(x => [x.repo, x.id]))
71
+ Object.assign(repo2id, Object.fromEntries(mapPlugins(x => [x.getData().repo, x.id]))) // started ones
72
+ apiGithub('search/repositories?q=topic:hfs-plugin+' + encodeURI(text)).then(res => {
73
+ const jobs = []
74
+ for (const it of res.items) {
75
+ const repo = it.full_name
76
+ const job = httpsString(`https://raw.githubusercontent.com/${repo}/master/${DIST_ROOT}plugin.js`).then(res => {
77
+ if (!res.ok)
78
+ throw res.statusCode
79
+ const pl = parsePluginSource(repo, res.body) // use 'repo' as 'id' client-side
80
+ if (!pl.apiRequired || pl.badApi) return
81
+ Object.assign(pl, { // inject some extra useful fields
82
+ downloading: downloading[repo],
83
+ installed: repo2id[repo]
84
+ })
85
+ list.add(pl)
86
+ // watch for events about this plugin, until this request is closed
87
+ ctx.req.on('close', onOff(events, {
88
+ pluginInstalled: p => {
89
+ if (p.repo === repo)
90
+ list.update({ id: repo }, { installed: true })
91
+ },
92
+ pluginUninstalled: id => {
93
+ if (repo === _.findKey(repo2id, x => x === id))
94
+ list.update({ id: repo }, { installed: false })
95
+ },
96
+ ['pluginDownload_'+repo](status) {
97
+ list.update({ id: repo }, { downloading: status ?? null })
98
+ }
99
+ }) )
100
+ })
101
+ jobs.push(job)
102
+ }
103
+ Promise.allSettled(jobs).then(() => list.end())
104
+ })
105
+ return list.return
106
+ },
107
+
108
+ async download_plugin({ id }) {
109
+ if (downloading[id])
110
+ return new ApiError(409, "already downloading")
111
+ downloadPlugin(id).then()
112
+ return {}
113
+ },
114
+
115
+}
116
+
117
+type DownloadStatus = true | undefined
118
+const downloading: Record<string, DownloadStatus> = {}
119
+
120
+function downloadProgress(id: string, status: DownloadStatus) {
121
+ if (status === undefined)
122
+ delete downloading[id]
123
+ else
124
+ downloading[id] = status
125
+ events.emit('pluginDownload_'+id, status)
126
+}
127
+
128
+async function downloadPlugin(repo: string) {
129
+ downloadProgress(repo, true)
130
+ const rec = await apiGithub('repos/'+repo)
131
+ const url = `https://github.com/${repo}/archive/${rec.default_branch}.zip`
132
+ const res = await httpsStream(url)
133
+ const repo2 = repo.split('/')[1] // second part, repo without the owner
134
+ const repo2clash = getAvailablePlugins().find(x => x.id === repo2) || mapPlugins(x => x.id === repo2).some(Boolean)
135
+ const pluginFolder = repo2clash ? repo.replace('/','-') : repo2 // longer form only if necessary
136
+ const installFolder = PLUGINS_PATH + '/' + pluginFolder
137
+ const GITHUB_ZIP_ROOT = repo2 + '-' + rec.default_branch // github puts everything within this folder
138
+ const rootWithinZip = GITHUB_ZIP_ROOT + '/' + DIST_ROOT
139
+ return new Promise(resolve =>
140
+ res.pipe(unzipper.Parse())
141
+ .on('entry', entry => {
142
+ const { path, type } = entry
143
+ if (!path.startsWith(rootWithinZip))
144
+ return entry.autodrain()
145
+ const dest = installFolder + '/' + path.slice(rootWithinZip.length)
146
+ if (type === 'File')
147
+ return entry.pipe(createWriteStream(dest))
148
+ mkdirSync(dest, { recursive: true }) // easy way be sure to have the folder ready before proceeding
149
+ })
150
+ .on('close', () => {
151
+ resolve(undefined)
152
+ downloadProgress(repo, undefined)
153
+ }))
154
}
155
156
export default apis
157
+
158
+function apiGithub(uri: string) {
159
+ return httpsString('https://api.github.com/'+uri, {
160
+ headers: {
161
+ 'User-Agent': 'HFS',
162
+ Accept: 'application/vnd.github.v3+json',
163
+ }
164
+ }).then(async res => {
165
+ if (!res.ok)
166
+ throw res.statusCode
167
+ return JSON.parse(res.body)
168
+ })
169
+}
server/src/apiMiddleware.ts
+9
-4
@@ -66,7 +66,7 @@ async function getJsonFromReq(req: IncomingMessage): Promise<any> {
66
}
67
68
// offer an api for a generic dynamic list
69
-export function sendList<T>(addAtStart: T[]=[]) {
69
+export function sendList<T>(addAtStart?: T[]) {
70
const stream = new Readable({ objectMode: true, read(){} })
71
const ret = {
72
return: stream,
@@ -75,15 +75,20 @@ export function sendList<T>(addAtStart: T[]=[]) {
75
update(search: Partial<T>, change: Partial<T>) {
76
stream.push({ update:[{ search, change }] })
77
},
78
+ end() { // notify end of additions
79
+ stream.push('end')
80
+ },
81
events(ctx: Koa.Context, eventMap: Parameters<typeof onOff>[1]) {
82
const off = onOff(events, eventMap)
83
ctx.res.once('close', off)
84
return stream
85
}
86
}
84
- for (const x of addAtStart)
85
- ret.add(x)
86
- stream.push('init')
87
+ if (addAtStart) {
88
+ for (const x of addAtStart)
89
+ ret.add(x)
90
+ stream.push('init')
91
+ }
92
return ret
93
}
94
server/src/misc.ts
+28
@@ -12,6 +12,9 @@ import { IS_WINDOWS } from './const'
12
import { execFile } from 'child_process'
13
import { Connection } from './connections'
14
import assert from 'assert'
15
+import https from 'node:https'
16
+import { RequestOptions } from 'https'
17
+import { IncomingMessage } from 'node:http'
18
19
export type Callback<IN=void, OUT=void> = (x:IN) => OUT
20
export type Dict<T = any> = Record<string, T>
@@ -295,3 +298,28 @@ export function same(a: any, b: any) {
298
}
299
catch { return false }
300
}
301
+
302
+export function httpsString(url: string, options:RequestOptions={}): Promise<IncomingMessage & { ok: boolean, body: string }> {
303
+ return httpsStream(url, options).then(res =>
304
+ new Promise(resolve => {
305
+ let buf = ''
306
+ res.on('data', chunk => buf += chunk.toString())
307
+ res.on('end', () => resolve(Object.assign(res, {
308
+ ok: (res.statusCode || 400) < 400,
309
+ body: buf
310
+ })))
311
+ })
312
+ )
313
+}
314
+
315
+export function httpsStream(url: string, options:RequestOptions={}): Promise<IncomingMessage> {
316
+ return new Promise((resolve, reject) => {
317
+ https.request(url, options, res => {
318
+ if (!res.statusCode || res.statusCode >= 400)
319
+ throw res
320
+ if (res.statusCode === 302 && res.headers.location)
321
+ return resolve(httpsStream(res.headers.location, options))
322
+ resolve(res)
323
+ }).on('error', reject).end()
324
+ })
325
+}
server/src/plugins.ts
+33
-13
@@ -17,7 +17,8 @@ import { readFile } from 'fs/promises'
17
import { existsSync, mkdirSync } from 'fs'
18
import { getConnections } from './connections'
19
20
-const PATH = 'plugins'
20
+export const PATH = 'plugins'
21
+export const DISABLING_POSTFIX = '-disabled'
22
23
const plugins: Record<string, Plugin> = {}
24
@@ -139,7 +140,15 @@ type PluginMiddleware = (ctx:Koa.Context) => void | Stop | CallMeAfter
140
type Stop = true
141
type CallMeAfter = ()=>void
142
142
-export interface AvailablePlugin { id: string, description?: string, version?: number, apiRequired?: number }
143
+export interface AvailablePlugin {
144
+ id: string
145
+ description?: string
146
+ version?: number
147
+ apiRequired?: number
148
+ repo?: string
149
+ badApi?: string
150
+}
151
+
152
let availablePlugins: Record<string, AvailablePlugin> = {}
153
154
export function getAvailablePlugins() {
@@ -163,17 +172,11 @@ async function rescan() {
172
const foundDisabled: typeof availablePlugins = {}
173
for (let f of await glob(PATH+'/*/plugin.js')) {
174
const id = f.split('/').slice(-2)[0]
166
- if (id.endsWith('-disabled')) continue
175
+ if (id.endsWith(DISABLING_POSTFIX)) continue
176
if (!enablePlugins.get().includes(id)) {
168
- const pl = foundDisabled[id] = { id } as typeof foundDisabled[0]
177
try {
178
const source = await readFile(f, 'utf8')
171
- const v = pl.description = /exports.description *= *"(.*)"/.exec(source)?.[1]
172
- if (v)
173
- try { pl.description = JSON.parse(`"${v}"`) }
174
- catch {}
175
- pl.version = Number(/exports.version *= *(\d+)/.exec(source)?.[1]) || undefined
176
- pl.apiRequired = Number(/exports.apiRequired *= *(\d+)/.exec(source)?.[1]) || undefined
179
+ foundDisabled[id] = parsePluginSource(id, source)
180
}
181
catch {}
182
continue
@@ -188,9 +191,7 @@ async function rescan() {
191
const { init, ...data } = await import(module)
192
delete data.default
193
deleteModule(require.resolve(module)) // avoid caching at next import
191
- data.badApi = data.apiRequired > API_VERSION ? "may not work correctly as it is designed for a newer version of HFS"
192
- : data.apiRequired < COMPATIBLE_API_VERSION ? "may not work correctly as it is designed for an older version of HFS"
193
- : undefined
194
+ calculateBadApi(data)
195
if (data.badApi)
196
console.log("plugin", id, data.badApi)
197
@@ -253,3 +254,22 @@ function deleteModule(id: string) {
254
255
onProcessExit(() =>
256
Promise.allSettled(mapPlugins(pl => pl.unload())))
257
+
258
+export function parsePluginSource(id: string, source: string) {
259
+ const pl: AvailablePlugin = { id }
260
+ const v = pl.description = /exports.description *= *"(.*)"/.exec(source)?.[1]
261
+ if (v)
262
+ try { pl.description = JSON.parse(`"${v}"`) }
263
+ catch {}
264
+ pl.repo = /exports.repo *= *"(.*)"/.exec(source)?.[1]
265
+ pl.version = Number(/exports.version *= *(\d+)/.exec(source)?.[1]) || undefined
266
+ pl.apiRequired = Number(/exports.apiRequired *= *(\d+)/.exec(source)?.[1]) || undefined
267
+ calculateBadApi(pl)
268
+ return pl
269
+}
270
+
271
+function calculateBadApi(data: AvailablePlugin) {
272
+ data.badApi = data.apiRequired! > API_VERSION ? "may not work correctly as it is designed for a newer version of HFS"
273
+ : data.apiRequired! < COMPATIBLE_API_VERSION ? "may not work correctly as it is designed for an older version of HFS"
274
+ : undefined
275
+}
todo.md
+5
-1
@@ -1,4 +1,9 @@
1
# To do
2
+- move logs to dedicated folder
3
+- admin: don't rely on google for font
4
+- fix: root without can_download breaks
5
+- use dialogs instead of side-forms on mobile (admin/fs+accounts)
6
+- easier deploy on cloud server
7
- admin/fs: sort items
8
- plugin.api.subscribeConfig
9
- watch certificates for change
@@ -24,7 +29,6 @@
29
- admin: warn before changing page if we have unsaved changes
30
- publish to npm (so people can "npm install hfs")
31
- frontend search supporting masks
27
-- use dialogs instead of side-forms on mobile (admin/fs+accounts)
32
- remove seconds from time
33
- plugin to download latest release
34
- https://api.github.com/repos/rejetto/hfs/releases/latest