@jsbot-tellus/core 2.4.0 → 2.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -5
- package/dist/app/cli.d.ts +10 -0
- package/dist/app/cli.d.ts.map +1 -0
- package/dist/app/cli.js +37 -0
- package/dist/app/cli.js.map +1 -0
- package/dist/app/runtime.d.ts +121 -0
- package/dist/app/runtime.d.ts.map +1 -0
- package/dist/app/runtime.js +296 -0
- package/dist/app/runtime.js.map +1 -0
- package/dist/app.js +63 -273
- package/dist/app.js.map +1 -1
- package/dist/core/dispatch/command.d.ts.map +1 -1
- package/dist/core/dispatch/command.js +5 -1
- package/dist/core/dispatch/command.js.map +1 -1
- package/dist/core/dispatch/router.d.ts.map +1 -1
- package/dist/core/dispatch/router.js +1 -1
- package/dist/core/dispatch/router.js.map +1 -1
- package/dist/core/plugin/command-params.d.ts +3 -36
- package/dist/core/plugin/command-params.d.ts.map +1 -1
- package/dist/core/plugin/command-params.js +2 -461
- package/dist/core/plugin/command-params.js.map +1 -1
- package/dist/core/plugin/install/paths.d.ts +7 -0
- package/dist/core/plugin/install/paths.d.ts.map +1 -0
- package/dist/core/plugin/install/paths.js +49 -0
- package/dist/core/plugin/install/paths.js.map +1 -0
- package/dist/core/plugin/install/state.d.ts +23 -0
- package/dist/core/plugin/install/state.d.ts.map +1 -0
- package/dist/core/plugin/install/state.js +129 -0
- package/dist/core/plugin/install/state.js.map +1 -0
- package/dist/core/plugin/install/validate-script.d.ts +3 -0
- package/dist/core/plugin/install/validate-script.d.ts.map +1 -0
- package/dist/core/plugin/install/validate-script.js +154 -0
- package/dist/core/plugin/install/validate-script.js.map +1 -0
- package/dist/core/plugin/install-dir.d.ts +3 -28
- package/dist/core/plugin/install-dir.d.ts.map +1 -1
- package/dist/core/plugin/install-dir.js +3 -326
- package/dist/core/plugin/install-dir.js.map +1 -1
- package/dist/core/plugin/installer/checks.d.ts +6 -0
- package/dist/core/plugin/installer/checks.d.ts.map +1 -0
- package/dist/core/plugin/installer/checks.js +99 -0
- package/dist/core/plugin/installer/checks.js.map +1 -0
- package/dist/core/plugin/installer/support.d.ts +37 -0
- package/dist/core/plugin/installer/support.d.ts.map +1 -0
- package/dist/core/plugin/installer/support.js +67 -0
- package/dist/core/plugin/installer/support.js.map +1 -0
- package/dist/core/plugin/installer/warnings.d.ts +5 -0
- package/dist/core/plugin/installer/warnings.d.ts.map +1 -0
- package/dist/core/plugin/installer/warnings.js +28 -0
- package/dist/core/plugin/installer/warnings.js.map +1 -0
- package/dist/core/plugin/installer.d.ts +4 -30
- package/dist/core/plugin/installer.d.ts.map +1 -1
- package/dist/core/plugin/installer.js +21 -202
- package/dist/core/plugin/installer.js.map +1 -1
- package/dist/core/plugin/params/compile.d.ts +3 -0
- package/dist/core/plugin/params/compile.d.ts.map +1 -0
- package/dist/core/plugin/params/compile.js +23 -0
- package/dist/core/plugin/params/compile.js.map +1 -0
- package/dist/core/plugin/params/declaration.d.ts +9 -0
- package/dist/core/plugin/params/declaration.d.ts.map +1 -0
- package/dist/core/plugin/params/declaration.js +223 -0
- package/dist/core/plugin/params/declaration.js.map +1 -0
- package/dist/core/plugin/params/input.d.ts +5 -0
- package/dist/core/plugin/params/input.d.ts.map +1 -0
- package/dist/core/plugin/params/input.js +181 -0
- package/dist/core/plugin/params/input.js.map +1 -0
- package/dist/core/plugin/params/reply.d.ts +3 -0
- package/dist/core/plugin/params/reply.d.ts.map +1 -0
- package/dist/core/plugin/params/reply.js +49 -0
- package/dist/core/plugin/params/reply.js.map +1 -0
- package/dist/core/plugin/params/type-level.d.ts +23 -0
- package/dist/core/plugin/params/type-level.d.ts.map +1 -0
- package/dist/core/plugin/params/type-level.js +2 -0
- package/dist/core/plugin/params/type-level.js.map +1 -0
- package/dist/core/plugin/params/types.d.ts +28 -0
- package/dist/core/plugin/params/types.d.ts.map +1 -0
- package/dist/core/plugin/params/types.js +12 -0
- package/dist/core/plugin/params/types.js.map +1 -0
- package/dist/core/plugin/params/values.d.ts +6 -0
- package/dist/core/plugin/params/values.d.ts.map +1 -0
- package/dist/core/plugin/params/values.js +108 -0
- package/dist/core/plugin/params/values.js.map +1 -0
- package/dist/core/web/api.d.ts +2 -14
- package/dist/core/web/api.d.ts.map +1 -1
- package/dist/core/web/api.js +8 -500
- package/dist/core/web/api.js.map +1 -1
- package/dist/core/web/routes/actions.d.ts +4 -0
- package/dist/core/web/routes/actions.d.ts.map +1 -0
- package/dist/core/web/routes/actions.js +33 -0
- package/dist/core/web/routes/actions.js.map +1 -0
- package/dist/core/web/routes/config.d.ts +5 -0
- package/dist/core/web/routes/config.d.ts.map +1 -0
- package/dist/core/web/routes/config.js +30 -0
- package/dist/core/web/routes/config.js.map +1 -0
- package/dist/core/web/routes/kv.d.ts +6 -0
- package/dist/core/web/routes/kv.d.ts.map +1 -0
- package/dist/core/web/routes/kv.js +80 -0
- package/dist/core/web/routes/kv.js.map +1 -0
- package/dist/core/web/routes/plugins.d.ts +5 -0
- package/dist/core/web/routes/plugins.d.ts.map +1 -0
- package/dist/core/web/routes/plugins.js +98 -0
- package/dist/core/web/routes/plugins.js.map +1 -0
- package/dist/core/web/routes/sessions.d.ts +8 -0
- package/dist/core/web/routes/sessions.d.ts.map +1 -0
- package/dist/core/web/routes/sessions.js +130 -0
- package/dist/core/web/routes/sessions.js.map +1 -0
- package/dist/core/web/routes/shared.d.ts +18 -0
- package/dist/core/web/routes/shared.d.ts.map +1 -0
- package/dist/core/web/routes/shared.js +19 -0
- package/dist/core/web/routes/shared.js.map +1 -0
- package/dist/core/web/routes/status.d.ts +6 -0
- package/dist/core/web/routes/status.d.ts.map +1 -0
- package/dist/core/web/routes/status.js +132 -0
- package/dist/core/web/routes/status.js.map +1 -0
- package/dist/core/web/server/auth.d.ts +17 -0
- package/dist/core/web/server/auth.d.ts.map +1 -0
- package/dist/core/web/server/auth.js +68 -0
- package/dist/core/web/server/auth.js.map +1 -0
- package/dist/core/web/server/csp.d.ts +11 -0
- package/dist/core/web/server/csp.d.ts.map +1 -0
- package/dist/core/web/server/csp.js +31 -0
- package/dist/core/web/server/csp.js.map +1 -0
- package/dist/core/web/server/request.d.ts +6 -0
- package/dist/core/web/server/request.d.ts.map +1 -0
- package/dist/core/web/server/request.js +33 -0
- package/dist/core/web/server/request.js.map +1 -0
- package/dist/core/web/server/security.d.ts +4 -0
- package/dist/core/web/server/security.d.ts.map +1 -0
- package/dist/core/web/server/security.js +12 -0
- package/dist/core/web/server/security.js.map +1 -0
- package/dist/core/web/server/sse.d.ts +18 -0
- package/dist/core/web/server/sse.d.ts.map +1 -0
- package/dist/core/web/server/sse.js +49 -0
- package/dist/core/web/server/sse.js.map +1 -0
- package/dist/core/web/server/static.d.ts +16 -0
- package/dist/core/web/server/static.d.ts.map +1 -0
- package/dist/core/web/server/static.js +80 -0
- package/dist/core/web/server/static.js.map +1 -0
- package/dist/core/web/server.d.ts +0 -7
- package/dist/core/web/server.d.ts.map +1 -1
- package/dist/core/web/server.js +41 -263
- package/dist/core/web/server.js.map +1 -1
- package/docs/architecture.md +28 -11
- package/docs/configuration.md +1 -1
- package/docs/migration.md +2 -2
- package/docs/plugin-api.md +3 -1
- package/docs/plugin-development.md +1 -1
- package/package.json +19 -17
- package/src/app/cli.ts +47 -0
- package/src/app/runtime.ts +522 -0
- package/src/app.ts +87 -395
- package/src/core/dispatch/command.ts +7 -1
- package/src/core/dispatch/router.ts +2 -1
- package/src/core/plugin/command-params.ts +8 -713
- package/src/core/plugin/install/paths.ts +72 -0
- package/src/core/plugin/install/state.ts +196 -0
- package/src/core/plugin/install/validate-script.ts +154 -0
- package/src/core/plugin/install-dir.ts +19 -427
- package/src/core/plugin/installer/checks.ts +137 -0
- package/src/core/plugin/installer/support.ts +120 -0
- package/src/core/plugin/installer/warnings.ts +52 -0
- package/src/core/plugin/installer.ts +52 -301
- package/src/core/plugin/params/compile.ts +41 -0
- package/src/core/plugin/params/declaration.ts +286 -0
- package/src/core/plugin/params/input.ts +254 -0
- package/src/core/plugin/params/reply.ts +59 -0
- package/src/core/plugin/params/type-level.ts +71 -0
- package/src/core/plugin/params/types.ts +49 -0
- package/src/core/plugin/params/values.ts +146 -0
- package/src/core/web/api.ts +33 -759
- package/src/core/web/routes/actions.ts +43 -0
- package/src/core/web/routes/config.ts +42 -0
- package/src/core/web/routes/kv.ts +122 -0
- package/src/core/web/routes/plugins.ts +151 -0
- package/src/core/web/routes/sessions.ts +188 -0
- package/src/core/web/routes/shared.ts +42 -0
- package/src/core/web/routes/status.ts +191 -0
- package/src/core/web/server/auth.ts +126 -0
- package/src/core/web/server/csp.ts +33 -0
- package/src/core/web/server/request.ts +38 -0
- package/src/core/web/server/security.ts +14 -0
- package/src/core/web/server/sse.ts +74 -0
- package/src/core/web/server/static.ts +112 -0
- package/src/core/web/server.ts +41 -383
|
@@ -1,427 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
export interface InstalledPackageRecord {
|
|
21
|
-
spec: string
|
|
22
|
-
version: string
|
|
23
|
-
pluginIds: string[]
|
|
24
|
-
dependencies: Record<string, string[]>
|
|
25
|
-
installedAt: string
|
|
26
|
-
/** Directory name under the plugins root. */
|
|
27
|
-
dir: string
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface InstallState {
|
|
31
|
-
version: 2
|
|
32
|
-
packages: Record<string, InstalledPackageRecord>
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export class InstallStateError extends Error {
|
|
36
|
-
constructor(message: string, options?: ErrorOptions) {
|
|
37
|
-
super(message, options)
|
|
38
|
-
this.name = 'InstallStateError'
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export function packageDirectoryName(name: string): string {
|
|
43
|
-
return name.startsWith('@') ? name.replace('/', '__') : name
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export async function readJsonFile(path: string): Promise<unknown> {
|
|
47
|
-
try {
|
|
48
|
-
return JSON.parse(await readFile(path, 'utf8'))
|
|
49
|
-
} catch (error) {
|
|
50
|
-
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined
|
|
51
|
-
|
|
52
|
-
throw error
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export async function readPackageVersion(
|
|
57
|
-
directory: string
|
|
58
|
-
): Promise<string | undefined> {
|
|
59
|
-
const manifest = await readJsonFile(join(directory, 'package.json')).catch(
|
|
60
|
-
() => undefined
|
|
61
|
-
)
|
|
62
|
-
|
|
63
|
-
if (typeof manifest !== 'object' || manifest === null) return undefined
|
|
64
|
-
|
|
65
|
-
const version = (manifest as Record<string, unknown>)['version']
|
|
66
|
-
|
|
67
|
-
return typeof version === 'string' ? version : undefined
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function isSafeDirectoryName(value: string): boolean {
|
|
71
|
-
return (
|
|
72
|
-
value.length > 0 &&
|
|
73
|
-
value !== '.' &&
|
|
74
|
-
value !== '..' &&
|
|
75
|
-
!value.includes('/') &&
|
|
76
|
-
!value.includes('\\')
|
|
77
|
-
)
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export async function readInstallState(dir: string): Promise<InstallState> {
|
|
81
|
-
const path = join(dir, INSTALL_STATE_FILE)
|
|
82
|
-
let value: unknown
|
|
83
|
-
|
|
84
|
-
try {
|
|
85
|
-
value = await readJsonFile(path)
|
|
86
|
-
} catch (error) {
|
|
87
|
-
throw new InstallStateError(`安装状态文件无法解析: ${path}`, {
|
|
88
|
-
cause: error,
|
|
89
|
-
})
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
if (value === undefined) {
|
|
93
|
-
return { version: 2, packages: {} }
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
97
|
-
throw new InstallStateError(`安装状态文件格式无效: ${path}`)
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if ((value as Record<string, unknown>)['version'] !== 2) {
|
|
101
|
-
throw new InstallStateError(`不支持的安装状态版本,请检查 ${path}`)
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const packages = (value as Record<string, unknown>)['packages']
|
|
105
|
-
|
|
106
|
-
if (
|
|
107
|
-
typeof packages !== 'object' ||
|
|
108
|
-
packages === null ||
|
|
109
|
-
Array.isArray(packages)
|
|
110
|
-
) {
|
|
111
|
-
throw new InstallStateError(`安装状态 packages 字段无效: ${path}`)
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const result: Record<string, InstalledPackageRecord> = {}
|
|
115
|
-
const directories = new Set<string>()
|
|
116
|
-
|
|
117
|
-
for (const [name, rawRecord] of Object.entries(packages)) {
|
|
118
|
-
if (
|
|
119
|
-
typeof rawRecord !== 'object' ||
|
|
120
|
-
rawRecord === null ||
|
|
121
|
-
Array.isArray(rawRecord)
|
|
122
|
-
) {
|
|
123
|
-
throw new InstallStateError(`安装状态记录无效: ${name}`)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const record = rawRecord as Record<string, unknown>
|
|
127
|
-
const pluginIds = record['pluginIds']
|
|
128
|
-
const dependencies = record['dependencies']
|
|
129
|
-
const directory = record['dir']
|
|
130
|
-
|
|
131
|
-
if (
|
|
132
|
-
typeof record['spec'] !== 'string' ||
|
|
133
|
-
typeof record['version'] !== 'string' ||
|
|
134
|
-
typeof record['installedAt'] !== 'string' ||
|
|
135
|
-
typeof directory !== 'string' ||
|
|
136
|
-
!isSafeDirectoryName(directory) ||
|
|
137
|
-
!Array.isArray(pluginIds) ||
|
|
138
|
-
pluginIds.some((item) => typeof item !== 'string')
|
|
139
|
-
) {
|
|
140
|
-
throw new InstallStateError(`安装状态记录不完整: ${name}`)
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
if (directories.has(directory)) {
|
|
144
|
-
throw new InstallStateError(
|
|
145
|
-
`安装状态存在重复的插件目录: ${directory}`
|
|
146
|
-
)
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
directories.add(directory)
|
|
150
|
-
|
|
151
|
-
if (
|
|
152
|
-
typeof dependencies !== 'object' ||
|
|
153
|
-
dependencies === null ||
|
|
154
|
-
Array.isArray(dependencies)
|
|
155
|
-
) {
|
|
156
|
-
throw new InstallStateError(`安装状态依赖字段无效: ${name}`)
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const parsedDependencies: Record<string, string[]> = {}
|
|
160
|
-
|
|
161
|
-
for (const [id, list] of Object.entries(dependencies)) {
|
|
162
|
-
if (
|
|
163
|
-
!Array.isArray(list) ||
|
|
164
|
-
list.some((item) => typeof item !== 'string')
|
|
165
|
-
) {
|
|
166
|
-
throw new InstallStateError(
|
|
167
|
-
`安装状态依赖记录无效: ${name}.${id}`
|
|
168
|
-
)
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
parsedDependencies[id] = [...list]
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
result[name] = {
|
|
175
|
-
spec: record['spec'],
|
|
176
|
-
version: record['version'],
|
|
177
|
-
pluginIds: [...pluginIds],
|
|
178
|
-
dependencies: parsedDependencies,
|
|
179
|
-
installedAt: record['installedAt'],
|
|
180
|
-
dir: directory,
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return { version: 2, packages: result }
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
export async function writeInstallState(
|
|
188
|
-
dir: string,
|
|
189
|
-
state: InstallState
|
|
190
|
-
): Promise<void> {
|
|
191
|
-
const sorted: InstallState = { version: 2, packages: {} }
|
|
192
|
-
|
|
193
|
-
for (const name of Object.keys(state.packages).sort()) {
|
|
194
|
-
const record = state.packages[name]
|
|
195
|
-
if (!record) continue
|
|
196
|
-
sorted.packages[name] = record
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const path = join(dir, INSTALL_STATE_FILE)
|
|
200
|
-
const temporary = `${path}.${randomUUID()}.tmp`
|
|
201
|
-
|
|
202
|
-
try {
|
|
203
|
-
await writeFile(
|
|
204
|
-
temporary,
|
|
205
|
-
`${JSON.stringify(sorted, null, 4)}\n`,
|
|
206
|
-
'utf8'
|
|
207
|
-
)
|
|
208
|
-
await rename(temporary, path)
|
|
209
|
-
} catch (error) {
|
|
210
|
-
await rm(temporary, { force: true }).catch(() => undefined)
|
|
211
|
-
throw error
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
export function corePackageRoot(): string {
|
|
216
|
-
let current = dirname(fileURLToPath(import.meta.url))
|
|
217
|
-
|
|
218
|
-
for (;;) {
|
|
219
|
-
try {
|
|
220
|
-
const manifest = JSON.parse(
|
|
221
|
-
readFileSync(join(current, 'package.json'), 'utf8')
|
|
222
|
-
) as Record<string, unknown>
|
|
223
|
-
|
|
224
|
-
if (manifest['name'] === CORE_PACKAGE_NAME) return current
|
|
225
|
-
} catch {
|
|
226
|
-
// keep walking up
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
const parent = dirname(current)
|
|
230
|
-
|
|
231
|
-
if (parent === current) {
|
|
232
|
-
throw new Error(`cannot locate ${CORE_PACKAGE_NAME} package root`)
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
current = parent
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
export async function ensureCoreLink(dir: string): Promise<void> {
|
|
240
|
-
const target = corePackageRoot()
|
|
241
|
-
const link = join(dir, 'node_modules', '@jsbot-tellus', 'core')
|
|
242
|
-
|
|
243
|
-
await mkdir(dirname(link), { recursive: true })
|
|
244
|
-
|
|
245
|
-
const existing = await realpath(link).catch(() => null)
|
|
246
|
-
|
|
247
|
-
if (existing !== null) {
|
|
248
|
-
const resolvedTarget = await realpath(target).catch(() => target)
|
|
249
|
-
|
|
250
|
-
if (existing === resolvedTarget) return
|
|
251
|
-
|
|
252
|
-
await rm(link, { recursive: true, force: true })
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
await symlink(
|
|
256
|
-
target,
|
|
257
|
-
link,
|
|
258
|
-
process.platform === 'win32' ? 'junction' : 'dir'
|
|
259
|
-
)
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
export async function pathExists(path: string): Promise<boolean> {
|
|
263
|
-
return (await stat(path).catch(() => null)) !== null
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
export async function ensureInstallRoot(root: string): Promise<void> {
|
|
267
|
-
await mkdir(root, { recursive: true })
|
|
268
|
-
await mkdir(join(root, '.jsbot'), { recursive: true })
|
|
269
|
-
await writeFile(
|
|
270
|
-
join(root, '.jsbot', VALIDATE_SCRIPT_FILE),
|
|
271
|
-
VALIDATE_SCRIPT,
|
|
272
|
-
'utf8'
|
|
273
|
-
)
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const VALIDATE_SCRIPT = `import { readdir } from 'node:fs/promises'
|
|
277
|
-
import { extname, join } from 'node:path'
|
|
278
|
-
import { pathToFileURL } from 'node:url'
|
|
279
|
-
|
|
280
|
-
const directory = process.argv[2]
|
|
281
|
-
|
|
282
|
-
if (!directory) {
|
|
283
|
-
console.error('missing plugin directory')
|
|
284
|
-
process.exit(2)
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const extensions = new Set(['.js', '.mjs', '.cjs'])
|
|
288
|
-
|
|
289
|
-
async function filesIn(directory) {
|
|
290
|
-
const entries = await readdir(directory, { withFileTypes: true })
|
|
291
|
-
entries.sort((a, b) => a.name.localeCompare(b.name))
|
|
292
|
-
|
|
293
|
-
const files = []
|
|
294
|
-
|
|
295
|
-
for (const entry of entries) {
|
|
296
|
-
if (entry.name.startsWith('.')) continue
|
|
297
|
-
|
|
298
|
-
const path = join(directory, entry.name)
|
|
299
|
-
|
|
300
|
-
if (entry.isDirectory()) {
|
|
301
|
-
if (entry.name === 'node_modules') continue
|
|
302
|
-
files.push(...(await filesIn(path)))
|
|
303
|
-
continue
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
if (
|
|
307
|
-
entry.isFile() &&
|
|
308
|
-
extensions.has(extname(entry.name)) &&
|
|
309
|
-
!/\\.d\\.(ts|mts|cts)$/.test(entry.name)
|
|
310
|
-
) {
|
|
311
|
-
files.push(path)
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
return files
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function isPlugin(value) {
|
|
319
|
-
if (typeof value !== 'object' || value === null) return false
|
|
320
|
-
if (!('manifest' in value) || !('setup' in value)) return false
|
|
321
|
-
|
|
322
|
-
const manifest = value.manifest
|
|
323
|
-
|
|
324
|
-
if (
|
|
325
|
-
typeof manifest !== 'object' ||
|
|
326
|
-
manifest === null ||
|
|
327
|
-
typeof manifest.id !== 'string' ||
|
|
328
|
-
typeof manifest.configSchema !== 'object' ||
|
|
329
|
-
manifest.configSchema === null ||
|
|
330
|
-
typeof manifest.configSchema.safeParse !== 'function' ||
|
|
331
|
-
typeof value.setup !== 'function'
|
|
332
|
-
) {
|
|
333
|
-
return false
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
if (
|
|
337
|
-
manifest.dependencies !== undefined &&
|
|
338
|
-
(!Array.isArray(manifest.dependencies) ||
|
|
339
|
-
manifest.dependencies.some((item) => typeof item !== 'string'))
|
|
340
|
-
) {
|
|
341
|
-
return false
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
return true
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
function collect(value, result) {
|
|
348
|
-
if (Array.isArray(value)) {
|
|
349
|
-
for (const item of value) collect(item, result)
|
|
350
|
-
return
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
if (isPlugin(value)) {
|
|
354
|
-
result.push(value)
|
|
355
|
-
return
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
if (
|
|
359
|
-
typeof value === 'object' &&
|
|
360
|
-
value !== null &&
|
|
361
|
-
'plugins' in value &&
|
|
362
|
-
Array.isArray(value.plugins)
|
|
363
|
-
) {
|
|
364
|
-
for (const item of value.plugins) collect(item, result)
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
const files = await filesIn(directory).catch((error) => {
|
|
369
|
-
console.error(error instanceof Error ? error.message : String(error))
|
|
370
|
-
process.exit(3)
|
|
371
|
-
})
|
|
372
|
-
|
|
373
|
-
if (files.length === 0) {
|
|
374
|
-
console.error('插件 dist 目录中没有可加载的文件')
|
|
375
|
-
process.exit(4)
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
const found = []
|
|
379
|
-
const invalid = []
|
|
380
|
-
|
|
381
|
-
for (const file of files) {
|
|
382
|
-
let imported
|
|
383
|
-
|
|
384
|
-
try {
|
|
385
|
-
imported = await import(pathToFileURL(file).href)
|
|
386
|
-
} catch (error) {
|
|
387
|
-
invalid.push(
|
|
388
|
-
\`failed to import \${file}: \${error instanceof Error ? error.message : String(error)}\`
|
|
389
|
-
)
|
|
390
|
-
continue
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
if (typeof imported !== 'object' || imported === null) continue
|
|
394
|
-
|
|
395
|
-
for (const candidate of [imported.default, imported.plugin, imported.plugins]) {
|
|
396
|
-
collect(candidate, found)
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
const unique = found.filter((plugin, index) => found.indexOf(plugin) === index)
|
|
401
|
-
|
|
402
|
-
if (unique.length === 0) {
|
|
403
|
-
console.error(
|
|
404
|
-
invalid[0] ??
|
|
405
|
-
'package does not export a JsBot plugin (default / plugin / plugins)'
|
|
406
|
-
)
|
|
407
|
-
process.exit(5)
|
|
408
|
-
}
|
|
409
|
-
|
|
410
|
-
console.log(
|
|
411
|
-
'__JSBOT_PLUGIN__' +
|
|
412
|
-
JSON.stringify(
|
|
413
|
-
unique.map((plugin) => ({
|
|
414
|
-
id: plugin.manifest.id,
|
|
415
|
-
version:
|
|
416
|
-
typeof plugin.manifest.version === 'string'
|
|
417
|
-
? plugin.manifest.version
|
|
418
|
-
: '0.0.0',
|
|
419
|
-
dependencies: Array.isArray(plugin.manifest.dependencies)
|
|
420
|
-
? plugin.manifest.dependencies.filter(
|
|
421
|
-
(item) => typeof item === 'string'
|
|
422
|
-
)
|
|
423
|
-
: [],
|
|
424
|
-
}))
|
|
425
|
-
)
|
|
426
|
-
)
|
|
427
|
-
`
|
|
1
|
+
export {
|
|
2
|
+
CORE_PACKAGE_NAME,
|
|
3
|
+
corePackageRoot,
|
|
4
|
+
ensureCoreLink,
|
|
5
|
+
ensureInstallRoot,
|
|
6
|
+
packageDirectoryName,
|
|
7
|
+
pathExists,
|
|
8
|
+
} from './install/paths.js'
|
|
9
|
+
export {
|
|
10
|
+
INSTALL_STATE_FILE,
|
|
11
|
+
InstallStateError,
|
|
12
|
+
readInstallState,
|
|
13
|
+
readJsonFile,
|
|
14
|
+
readPackageVersion,
|
|
15
|
+
writeInstallState,
|
|
16
|
+
type InstalledPackageRecord,
|
|
17
|
+
type InstallState,
|
|
18
|
+
} from './install/state.js'
|
|
19
|
+
export { VALIDATE_SCRIPT_FILE } from './install/validate-script.js'
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import type { InstalledPackageRecord, InstallState } from '../install-dir.js'
|
|
2
|
+
import { pathExists } from '../install-dir.js'
|
|
3
|
+
import type { ValidatedPlugin } from '../install-validate.js'
|
|
4
|
+
import { ID_PATTERN, InstallerError } from './support.js'
|
|
5
|
+
|
|
6
|
+
export function checkPlugins(
|
|
7
|
+
packageName: string,
|
|
8
|
+
plugins: ValidatedPlugin[],
|
|
9
|
+
state: InstallState,
|
|
10
|
+
reservedIds: () => ReadonlySet<string>
|
|
11
|
+
): void {
|
|
12
|
+
const ownIds = new Set(state.packages[packageName]?.pluginIds ?? [])
|
|
13
|
+
const ids = new Set<string>()
|
|
14
|
+
const taken = new Set<string>()
|
|
15
|
+
|
|
16
|
+
for (const id of reservedIds()) {
|
|
17
|
+
if (!ownIds.has(id)) taken.add(id)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
for (const [name, record] of Object.entries(state.packages)) {
|
|
21
|
+
if (name === packageName) continue
|
|
22
|
+
|
|
23
|
+
for (const id of record.pluginIds) taken.add(id)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
for (const plugin of plugins) {
|
|
27
|
+
if (!ID_PATTERN.test(plugin.id)) {
|
|
28
|
+
throw new InstallerError(`插件 id 无效: ${plugin.id}`)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (ids.has(plugin.id)) {
|
|
32
|
+
throw new InstallerError(`插件 id 重复: ${plugin.id}`)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (taken.has(plugin.id)) {
|
|
36
|
+
throw new InstallerError(
|
|
37
|
+
`插件 id 已被占用: ${plugin.id}(内置插件或其他已安装包)`
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
ids.add(plugin.id)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const graph = new Map<string, string[]>()
|
|
45
|
+
|
|
46
|
+
for (const [name, record] of Object.entries(state.packages)) {
|
|
47
|
+
if (name === packageName) continue
|
|
48
|
+
|
|
49
|
+
for (const id of record.pluginIds) {
|
|
50
|
+
graph.set(id, record.dependencies[id] ?? [])
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const plugin of plugins) {
|
|
55
|
+
graph.set(plugin.id, plugin.dependencies)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const reserved = reservedIds()
|
|
59
|
+
const available = new Set(graph.keys())
|
|
60
|
+
|
|
61
|
+
for (const id of reserved) {
|
|
62
|
+
if (!ownIds.has(id)) available.add(id)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const [id, dependencies] of graph) {
|
|
66
|
+
for (const dependency of dependencies) {
|
|
67
|
+
if (!available.has(dependency)) {
|
|
68
|
+
throw new InstallerError(
|
|
69
|
+
`插件 ${id} 依赖未加载的插件: ${dependency}`
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const visiting = new Set<string>()
|
|
76
|
+
const visited = new Set<string>()
|
|
77
|
+
|
|
78
|
+
const visit = (id: string): void => {
|
|
79
|
+
if (visited.has(id)) return
|
|
80
|
+
|
|
81
|
+
if (visiting.has(id)) {
|
|
82
|
+
throw new InstallerError(`插件依赖存在循环: ${id}`)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
visiting.add(id)
|
|
86
|
+
|
|
87
|
+
for (const dependency of graph.get(id) ?? []) {
|
|
88
|
+
if (graph.has(dependency)) visit(dependency)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
visiting.delete(id)
|
|
92
|
+
visited.add(id)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (const id of graph.keys()) visit(id)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function resolveTarget(target: string, state: InstallState): string {
|
|
99
|
+
const value = target.trim()
|
|
100
|
+
|
|
101
|
+
if (value.length === 0) {
|
|
102
|
+
throw new InstallerError('请提供包名或插件 id')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (state.packages[value] !== undefined) return value
|
|
106
|
+
|
|
107
|
+
for (const [name, record] of Object.entries(state.packages)) {
|
|
108
|
+
if (record.pluginIds.includes(value)) return name
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
throw new InstallerError(`未找到已安装的插件包: ${value}`)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export async function assertTargetAvailable(
|
|
115
|
+
state: InstallState,
|
|
116
|
+
packageName: string,
|
|
117
|
+
directory: string,
|
|
118
|
+
target: string,
|
|
119
|
+
previous: InstalledPackageRecord | undefined
|
|
120
|
+
): Promise<void> {
|
|
121
|
+
for (const [name, record] of Object.entries(state.packages)) {
|
|
122
|
+
if (name === packageName) continue
|
|
123
|
+
if (record.dir !== directory) continue
|
|
124
|
+
|
|
125
|
+
throw new InstallerError(
|
|
126
|
+
`插件目录已被其它已安装包使用: ${record.dir}(${name})`
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (previous !== undefined) return
|
|
131
|
+
|
|
132
|
+
if (await pathExists(target)) {
|
|
133
|
+
throw new InstallerError(
|
|
134
|
+
`安装目录已存在且不是由安装器管理: ${target}。请先移除该本地插件或为其改名。`
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import type { InstallerConfig } from '../../config/schema.js'
|
|
4
|
+
import type { Logger } from '../../logging/logger.js'
|
|
5
|
+
import { packageDirectoryName } from '../install-dir.js'
|
|
6
|
+
import type { PackageValidator } from '../install-validate.js'
|
|
7
|
+
import type { NpmRunner } from '../npm-runner.js'
|
|
8
|
+
import {
|
|
9
|
+
isPackageAllowed,
|
|
10
|
+
parsePackageSpec,
|
|
11
|
+
PackageSpecError,
|
|
12
|
+
type PackageSpec,
|
|
13
|
+
} from '../spec.js'
|
|
14
|
+
import type { PluginInstaller } from '../types.js'
|
|
15
|
+
|
|
16
|
+
export class InstallerError extends Error {
|
|
17
|
+
constructor(message: string, options?: ErrorOptions) {
|
|
18
|
+
super(message, options)
|
|
19
|
+
this.name = 'InstallerError'
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface InstallerOptions {
|
|
24
|
+
/** Plugins root. Installed packages are placed in subdirectories here. */
|
|
25
|
+
dir: string
|
|
26
|
+
config: InstallerConfig
|
|
27
|
+
logger: Logger
|
|
28
|
+
runner?: NpmRunner | undefined
|
|
29
|
+
validate?: PackageValidator | undefined
|
|
30
|
+
reservedIds?: (() => ReadonlySet<string>) | undefined
|
|
31
|
+
configuredPluginIds?: (() => ReadonlySet<string>) | undefined
|
|
32
|
+
now?: (() => Date) | undefined
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/i
|
|
36
|
+
export const NPM_FLAGS = ['--no-audit', '--no-fund', '--loglevel=error']
|
|
37
|
+
export const STAGING_MANIFEST = {
|
|
38
|
+
name: '@jsbot-tellus/plugin-staging',
|
|
39
|
+
private: true,
|
|
40
|
+
version: '0.0.0',
|
|
41
|
+
type: 'module',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function messageOf(error: unknown): string {
|
|
45
|
+
return error instanceof Error ? error.message : String(error)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function asInstallerError(error: unknown): InstallerError {
|
|
49
|
+
if (error instanceof InstallerError) return error
|
|
50
|
+
|
|
51
|
+
if (error instanceof PackageSpecError) {
|
|
52
|
+
return new InstallerError(error.message, { cause: error })
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return new InstallerError(messageOf(error), { cause: error })
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function installDirectoryName(
|
|
59
|
+
packageName: string,
|
|
60
|
+
pluginIds: readonly string[]
|
|
61
|
+
): string {
|
|
62
|
+
const only = pluginIds.length === 1 ? pluginIds[0] : undefined
|
|
63
|
+
|
|
64
|
+
return only ?? packageDirectoryName(packageName)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function parseInstallerSpec(
|
|
68
|
+
spec: string,
|
|
69
|
+
allowedPackages: string[]
|
|
70
|
+
): PackageSpec {
|
|
71
|
+
const parsed = parsePackageSpec(spec)
|
|
72
|
+
|
|
73
|
+
if (!isPackageAllowed(parsed.name, allowedPackages)) {
|
|
74
|
+
throw new InstallerError(`包不在允许列表: ${parsed.name}`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return parsed
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function temporaryPath(dir: string, kind: 'staging' | 'backup'): string {
|
|
81
|
+
return join(dir, `.${kind}-${randomUUID()}`)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function assertInstallerEnabled(enabled: boolean): void {
|
|
85
|
+
if (!enabled) {
|
|
86
|
+
throw new InstallerError(
|
|
87
|
+
'插件安装功能未启用(installer.enabled=false)'
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function createTaskSerializer(): <T>(
|
|
93
|
+
task: () => Promise<T>
|
|
94
|
+
) => Promise<T> {
|
|
95
|
+
let tail: Promise<unknown> = Promise.resolve()
|
|
96
|
+
|
|
97
|
+
return <T>(task: () => Promise<T>): Promise<T> => {
|
|
98
|
+
const run = tail.then(task, task)
|
|
99
|
+
|
|
100
|
+
tail = run.then(
|
|
101
|
+
() => undefined,
|
|
102
|
+
() => undefined
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
return run
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function createDisabledInstaller(): PluginInstaller {
|
|
110
|
+
const fail = (): Promise<never> =>
|
|
111
|
+
Promise.reject(new InstallerError('插件安装功能未启用'))
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
enabled: false,
|
|
115
|
+
list: () => Promise.resolve([]),
|
|
116
|
+
install: fail,
|
|
117
|
+
remove: fail,
|
|
118
|
+
update: fail,
|
|
119
|
+
}
|
|
120
|
+
}
|