@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.
Files changed (39) hide show
  1. package/README.md +14 -14
  2. package/dist/app.js +4 -2
  3. package/dist/app.js.map +1 -1
  4. package/dist/core/dispatch/command.d.ts.map +1 -1
  5. package/dist/core/dispatch/command.js.map +1 -1
  6. package/dist/core/index.d.ts +1 -0
  7. package/dist/core/index.d.ts.map +1 -1
  8. package/dist/core/index.js.map +1 -1
  9. package/dist/core/kernel.d.ts.map +1 -1
  10. package/dist/core/kernel.js +5 -1
  11. package/dist/core/kernel.js.map +1 -1
  12. package/dist/core/plugin/command-params.d.ts +22 -0
  13. package/dist/core/plugin/command-params.d.ts.map +1 -1
  14. package/dist/core/plugin/context.d.ts.map +1 -1
  15. package/dist/core/plugin/context.js.map +1 -1
  16. package/dist/core/plugin/loader.d.ts +13 -2
  17. package/dist/core/plugin/loader.d.ts.map +1 -1
  18. package/dist/core/plugin/loader.js +87 -5
  19. package/dist/core/plugin/loader.js.map +1 -1
  20. package/dist/core/plugin/types.d.ts +8 -7
  21. package/dist/core/plugin/types.d.ts.map +1 -1
  22. package/dist/core/plugin/types.js.map +1 -1
  23. package/dist/plugins/core/index.js +1 -1
  24. package/dist/plugins/core/index.js.map +1 -1
  25. package/docs/architecture.md +3 -3
  26. package/docs/configuration.md +3 -3
  27. package/docs/feature-parity.md +2 -2
  28. package/docs/plugin-api.md +11 -4
  29. package/docs/plugin-development.md +34 -24
  30. package/package.json +3 -1
  31. package/src/app.ts +4 -2
  32. package/src/core/dispatch/command.ts +9 -5
  33. package/src/core/index.ts +1 -0
  34. package/src/core/kernel.ts +8 -1
  35. package/src/core/plugin/command-params.ts +72 -0
  36. package/src/core/plugin/context.ts +9 -5
  37. package/src/core/plugin/loader.ts +150 -7
  38. package/src/core/plugin/types.ts +17 -11
  39. 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 dist = join(absolute, entry.name, 'dist')
83
- const distInfo = await stat(dist).catch(() => null)
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
- logger?.warn(
87
- `skipping local plugin ${entry.name}: dist directory not found`
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
- continue
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)))
@@ -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?: string | undefined
84
+ params?: Declaration | undefined
84
85
  }
85
86
 
86
87
  export interface CommandGroupOptions<
87
- TArgs extends unknown[] = [],
88
- > extends CommandOptions {
89
- handler?: CommandHandler<OneBotMessageEvent, TArgs> | undefined
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<TArgs extends unknown[] = []>(
94
- options: CommandOptions,
95
- handler: CommandHandler<OneBotMessageEvent, TArgs>
96
+ register<Declaration extends string = string>(
97
+ options: CommandOptions<Declaration>,
98
+ handler: CommandHandler<
99
+ OneBotMessageEvent,
100
+ CommandParamArgs<Declaration>
101
+ >
96
102
  ): () => void
97
- group<TArgs extends unknown[] = []>(
98
- options: CommandGroupOptions<TArgs>,
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
  )