@jsbot-tellus/core 2.1.0 → 2.2.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/README.md +14 -14
- package/dist/app.js +4 -2
- package/dist/app.js.map +1 -1
- package/dist/core/dispatch/command.d.ts.map +1 -1
- package/dist/core/dispatch/command.js.map +1 -1
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/kernel.d.ts.map +1 -1
- package/dist/core/kernel.js +5 -1
- package/dist/core/kernel.js.map +1 -1
- package/dist/core/plugin/command-params.d.ts +22 -0
- package/dist/core/plugin/command-params.d.ts.map +1 -1
- package/dist/core/plugin/context.d.ts.map +1 -1
- package/dist/core/plugin/context.js.map +1 -1
- package/dist/core/plugin/loader.d.ts +13 -2
- package/dist/core/plugin/loader.d.ts.map +1 -1
- package/dist/core/plugin/loader.js +87 -5
- package/dist/core/plugin/loader.js.map +1 -1
- package/dist/core/plugin/types.d.ts +8 -7
- package/dist/core/plugin/types.d.ts.map +1 -1
- package/dist/core/plugin/types.js.map +1 -1
- package/dist/plugins/core/index.js +1 -1
- package/dist/plugins/core/index.js.map +1 -1
- package/docs/architecture.md +3 -3
- package/docs/configuration.md +3 -3
- package/docs/feature-parity.md +2 -2
- package/docs/plugin-api.md +11 -4
- package/docs/plugin-development.md +34 -24
- package/package.json +3 -1
- package/src/app.ts +4 -2
- package/src/core/dispatch/command.ts +9 -5
- package/src/core/index.ts +1 -0
- package/src/core/kernel.ts +8 -1
- package/src/core/plugin/command-params.ts +72 -0
- package/src/core/plugin/context.ts +9 -5
- package/src/core/plugin/loader.ts +150 -7
- package/src/core/plugin/types.ts +17 -11
- package/src/plugins/core/index.ts +1 -1
|
@@ -4,9 +4,103 @@ import { pathToFileURL } from 'node:url'
|
|
|
4
4
|
import fg from 'fast-glob'
|
|
5
5
|
import type { Logger } from '../logging/logger.js'
|
|
6
6
|
import { collectPluginExports } from './exports.js'
|
|
7
|
+
import { readInstallState, readJsonFile } from './install-dir.js'
|
|
8
|
+
import { NpmCliRunner, type NpmRunner } from './npm-runner.js'
|
|
7
9
|
import type { AnyPluginDefinition } from './types.js'
|
|
8
10
|
|
|
9
|
-
type LoaderLogger = Pick<Logger, 'warn' | 'error'>
|
|
11
|
+
type LoaderLogger = Pick<Logger, 'warn' | 'error'> & {
|
|
12
|
+
info?: Logger['info'] | undefined
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface LocalPluginLoaderOptions {
|
|
16
|
+
/** Build local plugins that have a build script but no dist. Defaults to true. */
|
|
17
|
+
autoBuild?: boolean | undefined
|
|
18
|
+
/** Timeout for a single local plugin build. Defaults to 120 seconds. */
|
|
19
|
+
buildTimeoutMs?: number | undefined
|
|
20
|
+
/** npm runner override, mainly for tests. */
|
|
21
|
+
runner?: NpmRunner | undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const DEFAULT_BUILD_TIMEOUT_MS = 120_000
|
|
25
|
+
|
|
26
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
27
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function directoryExists(path: string): Promise<boolean> {
|
|
31
|
+
const info = await stat(path).catch(() => null)
|
|
32
|
+
|
|
33
|
+
return info?.isDirectory() ?? false
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function readBuildScript(directory: string): Promise<string | undefined> {
|
|
37
|
+
const manifest = await readJsonFile(join(directory, 'package.json')).catch(
|
|
38
|
+
() => undefined
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if (!isRecord(manifest) || !isRecord(manifest['scripts'])) {
|
|
42
|
+
return undefined
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const build = manifest['scripts']['build']
|
|
46
|
+
|
|
47
|
+
return typeof build === 'string' && build.trim().length > 0
|
|
48
|
+
? build
|
|
49
|
+
: undefined
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function readManagedDirectories(
|
|
53
|
+
root: string
|
|
54
|
+
): Promise<Set<string> | null> {
|
|
55
|
+
try {
|
|
56
|
+
const state = await readInstallState(root)
|
|
57
|
+
|
|
58
|
+
return new Set(
|
|
59
|
+
Object.values(state.packages).map((record) => record.dir)
|
|
60
|
+
)
|
|
61
|
+
} catch {
|
|
62
|
+
return null
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function buildLocalPlugin(
|
|
67
|
+
directory: string,
|
|
68
|
+
name: string,
|
|
69
|
+
logger: LoaderLogger | undefined,
|
|
70
|
+
runner: NpmRunner,
|
|
71
|
+
timeoutMs: number
|
|
72
|
+
): Promise<boolean> {
|
|
73
|
+
if ((await readBuildScript(directory)) === undefined) {
|
|
74
|
+
logger?.warn(
|
|
75
|
+
`skipping local plugin ${name}: dist directory not found and package.json has no build script`
|
|
76
|
+
)
|
|
77
|
+
return false
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!(await directoryExists(join(directory, 'node_modules')))) {
|
|
81
|
+
logger?.warn(
|
|
82
|
+
`skipping local plugin ${name}: dist directory not found and node_modules is missing; run npm install --prefix ${directory}`
|
|
83
|
+
)
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
logger?.info?.(`building local plugin ${name}: dist directory not found`)
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
await runner.run(['run', 'build'], {
|
|
91
|
+
cwd: directory,
|
|
92
|
+
timeoutMs,
|
|
93
|
+
})
|
|
94
|
+
} catch (error) {
|
|
95
|
+
logger?.error(`failed to build local plugin ${name}`, error)
|
|
96
|
+
logger?.warn(`skipping local plugin ${name}: build failed`)
|
|
97
|
+
return false
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
logger?.info?.(`built local plugin ${name}`)
|
|
101
|
+
|
|
102
|
+
return true
|
|
103
|
+
}
|
|
10
104
|
|
|
11
105
|
export async function loadPluginsFromDirectory(
|
|
12
106
|
directory: string,
|
|
@@ -63,7 +157,8 @@ export async function loadPluginsFromDirectory(
|
|
|
63
157
|
|
|
64
158
|
export async function loadLocalPlugins(
|
|
65
159
|
root: string,
|
|
66
|
-
logger?: LoaderLogger | undefined
|
|
160
|
+
logger?: LoaderLogger | undefined,
|
|
161
|
+
options: LocalPluginLoaderOptions = {}
|
|
67
162
|
): Promise<AnyPluginDefinition[]> {
|
|
68
163
|
const absolute = resolve(root)
|
|
69
164
|
const info = await stat(absolute).catch(() => null)
|
|
@@ -75,18 +170,66 @@ export async function loadLocalPlugins(
|
|
|
75
170
|
)
|
|
76
171
|
const plugins: AnyPluginDefinition[] = []
|
|
77
172
|
|
|
173
|
+
let autoBuild = options.autoBuild !== false
|
|
174
|
+
let managed = new Set<string>()
|
|
175
|
+
|
|
176
|
+
if (autoBuild) {
|
|
177
|
+
const directories = await readManagedDirectories(absolute)
|
|
178
|
+
|
|
179
|
+
if (directories === null) {
|
|
180
|
+
logger?.warn(
|
|
181
|
+
'cannot read plugin install state; automatic local plugin build is disabled'
|
|
182
|
+
)
|
|
183
|
+
autoBuild = false
|
|
184
|
+
} else {
|
|
185
|
+
managed = directories
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const runner = options.runner ?? new NpmCliRunner()
|
|
190
|
+
const timeoutMs = options.buildTimeoutMs ?? DEFAULT_BUILD_TIMEOUT_MS
|
|
191
|
+
|
|
78
192
|
for (const entry of entries) {
|
|
79
193
|
if (!entry.isDirectory() || entry.name.startsWith('.')) continue
|
|
80
194
|
if (entry.name === 'node_modules') continue
|
|
81
195
|
|
|
82
|
-
const
|
|
83
|
-
const
|
|
196
|
+
const directory = join(absolute, entry.name)
|
|
197
|
+
const dist = join(directory, 'dist')
|
|
198
|
+
let distInfo = await stat(dist).catch(() => null)
|
|
84
199
|
|
|
85
200
|
if (!distInfo?.isDirectory()) {
|
|
86
|
-
|
|
87
|
-
|
|
201
|
+
if (!autoBuild) {
|
|
202
|
+
logger?.warn(
|
|
203
|
+
`skipping local plugin ${entry.name}: dist directory not found`
|
|
204
|
+
)
|
|
205
|
+
continue
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (managed.has(entry.name)) {
|
|
209
|
+
logger?.warn(
|
|
210
|
+
`skipping local plugin ${entry.name}: dist directory not found (installed package is expected to include dist)`
|
|
211
|
+
)
|
|
212
|
+
continue
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const built = await buildLocalPlugin(
|
|
216
|
+
directory,
|
|
217
|
+
entry.name,
|
|
218
|
+
logger,
|
|
219
|
+
runner,
|
|
220
|
+
timeoutMs
|
|
88
221
|
)
|
|
89
|
-
|
|
222
|
+
|
|
223
|
+
if (!built) continue
|
|
224
|
+
|
|
225
|
+
distInfo = await stat(dist).catch(() => null)
|
|
226
|
+
|
|
227
|
+
if (!distInfo?.isDirectory()) {
|
|
228
|
+
logger?.warn(
|
|
229
|
+
`skipping local plugin ${entry.name}: build finished but dist directory not found`
|
|
230
|
+
)
|
|
231
|
+
continue
|
|
232
|
+
}
|
|
90
233
|
}
|
|
91
234
|
|
|
92
235
|
plugins.push(...(await loadPluginsFromDirectory(dist, logger)))
|
package/src/core/plugin/types.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type { SendOptions } from '../messaging/sender.js'
|
|
|
13
13
|
import type { PluginKV } from '../plugin/kv.js'
|
|
14
14
|
import type { Permission } from '../security/permission.js'
|
|
15
15
|
import type { SessionStore } from '../storage/session.js'
|
|
16
|
+
import type { CommandParamArgs } from './command-params.js'
|
|
16
17
|
|
|
17
18
|
export type EventType = 'message' | 'notice' | 'request' | 'meta'
|
|
18
19
|
|
|
@@ -74,28 +75,33 @@ export type PluginMiddleware = (
|
|
|
74
75
|
next: () => Promise<void>
|
|
75
76
|
) => void | Promise<void>
|
|
76
77
|
|
|
77
|
-
export interface CommandOptions {
|
|
78
|
+
export interface CommandOptions<Declaration extends string = string> {
|
|
78
79
|
name: string
|
|
79
80
|
aliases?: string[] | undefined
|
|
80
81
|
prefixes?: string[] | undefined
|
|
81
82
|
permission?: Permission | undefined
|
|
82
83
|
description?: string | undefined
|
|
83
|
-
params?:
|
|
84
|
+
params?: Declaration | undefined
|
|
84
85
|
}
|
|
85
86
|
|
|
86
87
|
export interface CommandGroupOptions<
|
|
87
|
-
|
|
88
|
-
> extends CommandOptions {
|
|
89
|
-
handler?:
|
|
88
|
+
Declaration extends string = string,
|
|
89
|
+
> extends CommandOptions<Declaration> {
|
|
90
|
+
handler?:
|
|
91
|
+
| CommandHandler<OneBotMessageEvent, CommandParamArgs<Declaration>>
|
|
92
|
+
| undefined
|
|
90
93
|
}
|
|
91
94
|
|
|
92
95
|
export interface CommandGroupRegistrar {
|
|
93
|
-
register<
|
|
94
|
-
options: CommandOptions
|
|
95
|
-
handler: CommandHandler<
|
|
96
|
+
register<Declaration extends string = string>(
|
|
97
|
+
options: CommandOptions<Declaration>,
|
|
98
|
+
handler: CommandHandler<
|
|
99
|
+
OneBotMessageEvent,
|
|
100
|
+
CommandParamArgs<Declaration>
|
|
101
|
+
>
|
|
96
102
|
): () => void
|
|
97
|
-
group<
|
|
98
|
-
options: CommandGroupOptions<
|
|
103
|
+
group<Declaration extends string = string>(
|
|
104
|
+
options: CommandGroupOptions<Declaration>,
|
|
99
105
|
setup: (group: CommandGroupRegistrar) => void
|
|
100
106
|
): () => void
|
|
101
107
|
}
|
|
@@ -184,7 +190,7 @@ export interface CoreServices {
|
|
|
184
190
|
can(actual: Permission, required: Permission): boolean
|
|
185
191
|
}
|
|
186
192
|
commands: {
|
|
187
|
-
list(): ReadonlyArray<CommandInfo>
|
|
193
|
+
list(event?: SnowLumaEvent): ReadonlyArray<CommandInfo>
|
|
188
194
|
}
|
|
189
195
|
plugins: PluginStatus
|
|
190
196
|
installer: PluginInstaller
|
|
@@ -38,7 +38,7 @@ export const corePlugin = definePlugin({
|
|
|
38
38
|
async (event, context) => {
|
|
39
39
|
const role = ctx.core.roles.roleOf(event.user_id)
|
|
40
40
|
const commands = ctx.core.commands
|
|
41
|
-
.list()
|
|
41
|
+
.list(event)
|
|
42
42
|
.filter((command) =>
|
|
43
43
|
ctx.core.roles.can(role, command.permission)
|
|
44
44
|
)
|