@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.
Files changed (183) hide show
  1. package/README.md +8 -5
  2. package/dist/app/cli.d.ts +10 -0
  3. package/dist/app/cli.d.ts.map +1 -0
  4. package/dist/app/cli.js +37 -0
  5. package/dist/app/cli.js.map +1 -0
  6. package/dist/app/runtime.d.ts +121 -0
  7. package/dist/app/runtime.d.ts.map +1 -0
  8. package/dist/app/runtime.js +296 -0
  9. package/dist/app/runtime.js.map +1 -0
  10. package/dist/app.js +63 -273
  11. package/dist/app.js.map +1 -1
  12. package/dist/core/dispatch/command.d.ts.map +1 -1
  13. package/dist/core/dispatch/command.js +5 -1
  14. package/dist/core/dispatch/command.js.map +1 -1
  15. package/dist/core/dispatch/router.d.ts.map +1 -1
  16. package/dist/core/dispatch/router.js +1 -1
  17. package/dist/core/dispatch/router.js.map +1 -1
  18. package/dist/core/plugin/command-params.d.ts +3 -36
  19. package/dist/core/plugin/command-params.d.ts.map +1 -1
  20. package/dist/core/plugin/command-params.js +2 -461
  21. package/dist/core/plugin/command-params.js.map +1 -1
  22. package/dist/core/plugin/install/paths.d.ts +7 -0
  23. package/dist/core/plugin/install/paths.d.ts.map +1 -0
  24. package/dist/core/plugin/install/paths.js +49 -0
  25. package/dist/core/plugin/install/paths.js.map +1 -0
  26. package/dist/core/plugin/install/state.d.ts +23 -0
  27. package/dist/core/plugin/install/state.d.ts.map +1 -0
  28. package/dist/core/plugin/install/state.js +129 -0
  29. package/dist/core/plugin/install/state.js.map +1 -0
  30. package/dist/core/plugin/install/validate-script.d.ts +3 -0
  31. package/dist/core/plugin/install/validate-script.d.ts.map +1 -0
  32. package/dist/core/plugin/install/validate-script.js +154 -0
  33. package/dist/core/plugin/install/validate-script.js.map +1 -0
  34. package/dist/core/plugin/install-dir.d.ts +3 -28
  35. package/dist/core/plugin/install-dir.d.ts.map +1 -1
  36. package/dist/core/plugin/install-dir.js +3 -326
  37. package/dist/core/plugin/install-dir.js.map +1 -1
  38. package/dist/core/plugin/installer/checks.d.ts +6 -0
  39. package/dist/core/plugin/installer/checks.d.ts.map +1 -0
  40. package/dist/core/plugin/installer/checks.js +99 -0
  41. package/dist/core/plugin/installer/checks.js.map +1 -0
  42. package/dist/core/plugin/installer/support.d.ts +37 -0
  43. package/dist/core/plugin/installer/support.d.ts.map +1 -0
  44. package/dist/core/plugin/installer/support.js +67 -0
  45. package/dist/core/plugin/installer/support.js.map +1 -0
  46. package/dist/core/plugin/installer/warnings.d.ts +5 -0
  47. package/dist/core/plugin/installer/warnings.d.ts.map +1 -0
  48. package/dist/core/plugin/installer/warnings.js +28 -0
  49. package/dist/core/plugin/installer/warnings.js.map +1 -0
  50. package/dist/core/plugin/installer.d.ts +4 -30
  51. package/dist/core/plugin/installer.d.ts.map +1 -1
  52. package/dist/core/plugin/installer.js +21 -202
  53. package/dist/core/plugin/installer.js.map +1 -1
  54. package/dist/core/plugin/params/compile.d.ts +3 -0
  55. package/dist/core/plugin/params/compile.d.ts.map +1 -0
  56. package/dist/core/plugin/params/compile.js +23 -0
  57. package/dist/core/plugin/params/compile.js.map +1 -0
  58. package/dist/core/plugin/params/declaration.d.ts +9 -0
  59. package/dist/core/plugin/params/declaration.d.ts.map +1 -0
  60. package/dist/core/plugin/params/declaration.js +223 -0
  61. package/dist/core/plugin/params/declaration.js.map +1 -0
  62. package/dist/core/plugin/params/input.d.ts +5 -0
  63. package/dist/core/plugin/params/input.d.ts.map +1 -0
  64. package/dist/core/plugin/params/input.js +181 -0
  65. package/dist/core/plugin/params/input.js.map +1 -0
  66. package/dist/core/plugin/params/reply.d.ts +3 -0
  67. package/dist/core/plugin/params/reply.d.ts.map +1 -0
  68. package/dist/core/plugin/params/reply.js +49 -0
  69. package/dist/core/plugin/params/reply.js.map +1 -0
  70. package/dist/core/plugin/params/type-level.d.ts +23 -0
  71. package/dist/core/plugin/params/type-level.d.ts.map +1 -0
  72. package/dist/core/plugin/params/type-level.js +2 -0
  73. package/dist/core/plugin/params/type-level.js.map +1 -0
  74. package/dist/core/plugin/params/types.d.ts +28 -0
  75. package/dist/core/plugin/params/types.d.ts.map +1 -0
  76. package/dist/core/plugin/params/types.js +12 -0
  77. package/dist/core/plugin/params/types.js.map +1 -0
  78. package/dist/core/plugin/params/values.d.ts +6 -0
  79. package/dist/core/plugin/params/values.d.ts.map +1 -0
  80. package/dist/core/plugin/params/values.js +108 -0
  81. package/dist/core/plugin/params/values.js.map +1 -0
  82. package/dist/core/web/api.d.ts +2 -14
  83. package/dist/core/web/api.d.ts.map +1 -1
  84. package/dist/core/web/api.js +8 -500
  85. package/dist/core/web/api.js.map +1 -1
  86. package/dist/core/web/routes/actions.d.ts +4 -0
  87. package/dist/core/web/routes/actions.d.ts.map +1 -0
  88. package/dist/core/web/routes/actions.js +33 -0
  89. package/dist/core/web/routes/actions.js.map +1 -0
  90. package/dist/core/web/routes/config.d.ts +5 -0
  91. package/dist/core/web/routes/config.d.ts.map +1 -0
  92. package/dist/core/web/routes/config.js +30 -0
  93. package/dist/core/web/routes/config.js.map +1 -0
  94. package/dist/core/web/routes/kv.d.ts +6 -0
  95. package/dist/core/web/routes/kv.d.ts.map +1 -0
  96. package/dist/core/web/routes/kv.js +80 -0
  97. package/dist/core/web/routes/kv.js.map +1 -0
  98. package/dist/core/web/routes/plugins.d.ts +5 -0
  99. package/dist/core/web/routes/plugins.d.ts.map +1 -0
  100. package/dist/core/web/routes/plugins.js +98 -0
  101. package/dist/core/web/routes/plugins.js.map +1 -0
  102. package/dist/core/web/routes/sessions.d.ts +8 -0
  103. package/dist/core/web/routes/sessions.d.ts.map +1 -0
  104. package/dist/core/web/routes/sessions.js +130 -0
  105. package/dist/core/web/routes/sessions.js.map +1 -0
  106. package/dist/core/web/routes/shared.d.ts +18 -0
  107. package/dist/core/web/routes/shared.d.ts.map +1 -0
  108. package/dist/core/web/routes/shared.js +19 -0
  109. package/dist/core/web/routes/shared.js.map +1 -0
  110. package/dist/core/web/routes/status.d.ts +6 -0
  111. package/dist/core/web/routes/status.d.ts.map +1 -0
  112. package/dist/core/web/routes/status.js +132 -0
  113. package/dist/core/web/routes/status.js.map +1 -0
  114. package/dist/core/web/server/auth.d.ts +17 -0
  115. package/dist/core/web/server/auth.d.ts.map +1 -0
  116. package/dist/core/web/server/auth.js +68 -0
  117. package/dist/core/web/server/auth.js.map +1 -0
  118. package/dist/core/web/server/csp.d.ts +11 -0
  119. package/dist/core/web/server/csp.d.ts.map +1 -0
  120. package/dist/core/web/server/csp.js +31 -0
  121. package/dist/core/web/server/csp.js.map +1 -0
  122. package/dist/core/web/server/request.d.ts +6 -0
  123. package/dist/core/web/server/request.d.ts.map +1 -0
  124. package/dist/core/web/server/request.js +33 -0
  125. package/dist/core/web/server/request.js.map +1 -0
  126. package/dist/core/web/server/security.d.ts +4 -0
  127. package/dist/core/web/server/security.d.ts.map +1 -0
  128. package/dist/core/web/server/security.js +12 -0
  129. package/dist/core/web/server/security.js.map +1 -0
  130. package/dist/core/web/server/sse.d.ts +18 -0
  131. package/dist/core/web/server/sse.d.ts.map +1 -0
  132. package/dist/core/web/server/sse.js +49 -0
  133. package/dist/core/web/server/sse.js.map +1 -0
  134. package/dist/core/web/server/static.d.ts +16 -0
  135. package/dist/core/web/server/static.d.ts.map +1 -0
  136. package/dist/core/web/server/static.js +80 -0
  137. package/dist/core/web/server/static.js.map +1 -0
  138. package/dist/core/web/server.d.ts +0 -7
  139. package/dist/core/web/server.d.ts.map +1 -1
  140. package/dist/core/web/server.js +41 -263
  141. package/dist/core/web/server.js.map +1 -1
  142. package/docs/architecture.md +28 -11
  143. package/docs/configuration.md +1 -1
  144. package/docs/migration.md +2 -2
  145. package/docs/plugin-api.md +3 -1
  146. package/docs/plugin-development.md +1 -1
  147. package/package.json +19 -17
  148. package/src/app/cli.ts +47 -0
  149. package/src/app/runtime.ts +522 -0
  150. package/src/app.ts +87 -395
  151. package/src/core/dispatch/command.ts +7 -1
  152. package/src/core/dispatch/router.ts +2 -1
  153. package/src/core/plugin/command-params.ts +8 -713
  154. package/src/core/plugin/install/paths.ts +72 -0
  155. package/src/core/plugin/install/state.ts +196 -0
  156. package/src/core/plugin/install/validate-script.ts +154 -0
  157. package/src/core/plugin/install-dir.ts +19 -427
  158. package/src/core/plugin/installer/checks.ts +137 -0
  159. package/src/core/plugin/installer/support.ts +120 -0
  160. package/src/core/plugin/installer/warnings.ts +52 -0
  161. package/src/core/plugin/installer.ts +52 -301
  162. package/src/core/plugin/params/compile.ts +41 -0
  163. package/src/core/plugin/params/declaration.ts +286 -0
  164. package/src/core/plugin/params/input.ts +254 -0
  165. package/src/core/plugin/params/reply.ts +59 -0
  166. package/src/core/plugin/params/type-level.ts +71 -0
  167. package/src/core/plugin/params/types.ts +49 -0
  168. package/src/core/plugin/params/values.ts +146 -0
  169. package/src/core/web/api.ts +33 -759
  170. package/src/core/web/routes/actions.ts +43 -0
  171. package/src/core/web/routes/config.ts +42 -0
  172. package/src/core/web/routes/kv.ts +122 -0
  173. package/src/core/web/routes/plugins.ts +151 -0
  174. package/src/core/web/routes/sessions.ts +188 -0
  175. package/src/core/web/routes/shared.ts +42 -0
  176. package/src/core/web/routes/status.ts +191 -0
  177. package/src/core/web/server/auth.ts +126 -0
  178. package/src/core/web/server/csp.ts +33 -0
  179. package/src/core/web/server/request.ts +38 -0
  180. package/src/core/web/server/security.ts +14 -0
  181. package/src/core/web/server/sse.ts +74 -0
  182. package/src/core/web/server/static.ts +112 -0
  183. package/src/core/web/server.ts +41 -383
package/src/app.ts CHANGED
@@ -1,280 +1,54 @@
1
1
  #!/usr/bin/env node
2
- import type { SnowLumaEvent } from '@snowluma/sdk'
3
- import { existsSync, readFileSync } from 'node:fs'
4
- import { dirname, join, resolve } from 'node:path'
5
- import { parseArgs } from 'node:util'
2
+ import { dirname } from 'node:path'
6
3
  import { fileURLToPath } from 'node:url'
7
- import { createBotClient } from './core/client.js'
8
- import type { AppConfig } from './core/config/schema.js'
4
+ import { parseCli, pick, USAGE } from './app/cli.js'
9
5
  import {
10
- ConfigError,
11
- loadConfig,
12
- resolveConfigPath,
13
- resolveDataDir,
14
- } from './core/config/loader.js'
15
- import { Kernel } from './core/kernel.js'
6
+ createAppLogging,
7
+ createExclusiveRunner,
8
+ createRestartRequester,
9
+ createRuntime,
10
+ createSerialRunner,
11
+ createShutdown,
12
+ createWebUiDeps,
13
+ credentialSignature,
14
+ describeError,
15
+ loadRuntimeSpec,
16
+ readVersion,
17
+ registerShutdownSignals,
18
+ resolveRuntimeSettings,
19
+ resolveStaticDir,
20
+ restartDrainTimeout,
21
+ startInitialRuntime,
22
+ type Runtime,
23
+ type StaticBinding,
24
+ } from './app/runtime.js'
25
+ import { ConfigError } from './core/config/loader.js'
26
+ import type { AppConfig } from './core/config/schema.js'
16
27
  import { ConfigStore } from './core/web/config-store.js'
17
- import { EventFeed } from './core/web/event-feed.js'
18
- import { LogBuffer } from './core/logging/log-buffer.js'
19
- import {
20
- createLogger,
21
- defaultLogSink,
22
- parseLogLevel,
23
- type LogSink,
24
- type Logger,
25
- } from './core/logging/logger.js'
26
- import { NpmPluginInstaller } from './core/plugin/installer.js'
27
- import { loadLocalPlugins } from './core/plugin/loader.js'
28
- import type {
29
- AnyPluginDefinition,
30
- PluginInstaller,
31
- } from './core/plugin/types.js'
32
- import { openDatabase, type Database } from './core/storage/database.js'
33
28
  import { WebUiServer } from './core/web/server.js'
34
- import type { WebUiAddress, WebUiDeps } from './core/web/types.js'
35
- import { builtinPlugins } from './plugins/index.js'
36
-
37
- const LOCAL_PLUGIN_DIR = 'plugins'
38
-
39
- const USAGE = `Usage: jsbot [options]
40
-
41
- Options:
42
- --config <path> Config file path (env: BOT_CONFIG)
43
- --data-dir <path> Data directory (env: BOT_DATA_DIR)
44
- --log-level <level> debug, info, warn or error (env: BOT_LOG_LEVEL)
45
- --help Show this help
46
-
47
- Config precedence: CLI > environment > ./bot.config.json.
48
- Data precedence: CLI > environment > ./data.
49
- Local plugins: ./${LOCAL_PLUGIN_DIR}/<id>/dist (auto-built when missing).
50
- `
51
-
52
- function pick(value: unknown): string | undefined {
53
- return typeof value === 'string' && value.trim().length > 0
54
- ? value
55
- : undefined
56
- }
29
+ import type { WebUiAddress } from './core/web/types.js'
57
30
 
58
- interface PluginSource {
59
- label: string
60
- plugins: AnyPluginDefinition[]
61
- }
62
-
63
- function mergePlugins(
64
- sources: PluginSource[],
65
- logger: Logger
66
- ): AnyPluginDefinition[] {
67
- const seen = new Set<string>()
68
- const plugins: AnyPluginDefinition[] = []
69
-
70
- for (const source of sources) {
71
- for (const plugin of source.plugins) {
72
- const id = plugin.manifest.id
73
-
74
- if (seen.has(id)) {
75
- logger.warn(
76
- `skipping ${source.label} plugin ${id}: plugin id already loaded`
77
- )
78
- continue
79
- }
80
-
81
- seen.add(id)
82
- plugins.push(plugin)
83
- }
84
- }
85
-
86
- return plugins
87
- }
88
-
89
- interface RuntimeSettings {
90
- logger: Logger
91
- configPath: string
92
- dataDir: string
93
- }
94
-
95
- interface RuntimeSpec {
96
- config: AppConfig
97
- dataDir: string
98
- pluginDir: string
99
- plugins: AnyPluginDefinition[]
100
- }
101
-
102
- interface Runtime {
103
- spec: RuntimeSpec
104
- kernel: Kernel
105
- database: Database
106
- installer: PluginInstaller
107
- }
108
-
109
- interface StaticBinding {
110
- host: string
111
- port: number
112
- }
113
-
114
- async function loadRuntimeSpec(
115
- settings: RuntimeSettings
116
- ): Promise<RuntimeSpec> {
117
- const { logger } = settings
118
-
119
- logger.info('loading config', settings.configPath)
120
-
121
- const config = await loadConfig(settings.configPath)
122
- const pluginDir = resolve(LOCAL_PLUGIN_DIR)
123
-
124
- logger.info('loading plugins from', pluginDir)
125
-
126
- const localPlugins = await loadLocalPlugins(pluginDir, logger, {
127
- buildTimeoutMs: config.installer.timeoutMs,
128
- })
129
-
130
- if (localPlugins.length > 0) {
131
- logger.info(`loaded ${localPlugins.length} plugin(s) from ${pluginDir}`)
132
- }
133
-
134
- const plugins = mergePlugins(
135
- [
136
- { label: 'built-in', plugins: builtinPlugins },
137
- { label: 'plugins', plugins: localPlugins },
138
- ],
139
- logger
140
- )
141
-
142
- return {
143
- config,
144
- dataDir: settings.dataDir,
145
- pluginDir,
146
- plugins,
147
- }
148
- }
149
-
150
- function createRuntime(
151
- spec: RuntimeSpec,
152
- settings: RuntimeSettings,
153
- onRestart: () => void,
154
- onEvent: (event: SnowLumaEvent) => void
155
- ): Runtime {
156
- const { logger } = settings
157
- const installer = new NpmPluginInstaller({
158
- dir: spec.pluginDir,
159
- config: spec.config.installer,
160
- logger: logger.child('installer'),
161
- reservedIds: () =>
162
- new Set(spec.plugins.map((plugin) => plugin.manifest.id)),
163
- configuredPluginIds: () => new Set(Object.keys(spec.config.plugins)),
164
- })
165
- const database = openDatabase({ dataDir: spec.dataDir })
166
-
167
- const client = createBotClient({
168
- url: spec.config.url,
169
- ...(spec.config.accessToken
170
- ? { accessToken: spec.config.accessToken }
171
- : {}),
172
- })
173
-
174
- const kernel = new Kernel({
175
- config: spec.config,
176
- client,
177
- logger,
178
- database,
179
- plugins: spec.plugins,
180
- installer,
181
- onRestart,
182
- onEvent,
183
- })
184
-
185
- return { spec, kernel, database, installer }
186
- }
187
-
188
- function describeError(error: unknown): string {
189
- if (error instanceof Error) {
190
- return error.stack ?? `${error.name}: ${error.message}`
191
- }
192
-
193
- return String(error)
194
- }
195
-
196
- function credentialSignature(config: AppConfig): string {
197
- const webui = config.webui
198
-
199
- return [webui.username ?? '', webui.password ?? ''].join(':')
200
- }
201
-
202
- function resolveStaticDir(): string | null {
203
- const scriptDir = dirname(fileURLToPath(import.meta.url))
204
- const candidates = [
205
- join(scriptDir, 'webui'),
206
- resolve(process.cwd(), 'dist/webui'),
207
- ]
208
-
209
- for (const candidate of candidates) {
210
- if (existsSync(join(candidate, 'index.html'))) return candidate
211
- }
212
-
213
- return null
214
- }
215
-
216
- function readVersion(): string {
217
- try {
218
- const pkgPath = new URL('../package.json', import.meta.url)
219
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as {
220
- version?: string
221
- }
222
-
223
- return pkg.version ?? 'unknown'
224
- } catch {
225
- return 'unknown'
226
- }
227
- }
31
+ const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url))
228
32
 
229
33
  async function main(): Promise<void> {
230
- const { values } = parseArgs({
231
- options: {
232
- config: { type: 'string' },
233
- 'data-dir': { type: 'string' },
234
- 'log-level': { type: 'string' },
235
- help: { type: 'boolean', short: 'h' },
236
- },
237
- allowPositionals: false,
238
- })
34
+ const cli = parseCli()
239
35
 
240
- if (values.help) {
36
+ if (cli.help) {
241
37
  process.stdout.write(USAGE)
242
38
  return
243
39
  }
244
40
 
245
- const logs = new LogBuffer()
246
- const events = new EventFeed()
247
- const tapEvents = (event: SnowLumaEvent): void => {
248
- events.add(event)
249
- }
250
- const sink: LogSink = (record) => {
251
- defaultLogSink(record)
252
- logs.push(record)
253
- }
254
-
255
- const logger = createLogger({
256
- level: parseLogLevel(
257
- pick(values['log-level']) ?? process.env.BOT_LOG_LEVEL
258
- ),
259
- scope: 'app',
260
- sink,
261
- })
41
+ const { logs, events, logger, tapEvents } = createAppLogging(
42
+ pick(cli.logLevel) ?? process.env.BOT_LOG_LEVEL
43
+ )
262
44
 
263
- const configPath = resolveConfigPath({
264
- config: pick(values.config),
45
+ const settings = resolveRuntimeSettings(logger, {
46
+ config: pick(cli.config),
47
+ dataDir: pick(cli.dataDir),
265
48
  env: process.env,
266
49
  })
267
50
 
268
- const dataDir = resolveDataDir({
269
- dataDir: pick(values['data-dir']),
270
- env: process.env,
271
- })
272
-
273
- const settings: RuntimeSettings = {
274
- logger,
275
- configPath,
276
- dataDir,
277
- }
51
+ const { configPath, dataDir } = settings
278
52
 
279
53
  // Config file errors remain fatal: there is nothing the WebUI can restore
280
54
  // from when the config cannot even be parsed.
@@ -287,19 +61,9 @@ async function main(): Promise<void> {
287
61
  let shuttingDown = false
288
62
  let restarting = false
289
63
  let restartPending = false
290
- let restartTimer: NodeJS.Timeout | null = null
291
- let restartTail: Promise<boolean> = Promise.resolve(true)
292
- let exclusiveTail: Promise<unknown> = Promise.resolve()
293
64
  let boundWebUi: StaticBinding | null = null
294
65
  let credentials = credentialSignature(initialSpec.config)
295
66
 
296
- const restartDrainTimeout = (previous: Runtime | null): number => {
297
- const installTimeout =
298
- previous?.spec.config.installer.timeoutMs ?? 5_000
299
-
300
- return Math.max(installTimeout, 5_000) * 2 + 5_000
301
- }
302
-
303
67
  const restartRuntimeOnce = async (): Promise<boolean> => {
304
68
  if (shuttingDown) return false
305
69
 
@@ -374,32 +138,18 @@ async function main(): Promise<void> {
374
138
  }
375
139
  }
376
140
 
377
- const restartRuntime = (): Promise<boolean> => {
378
- const run = restartTail.then(restartRuntimeOnce, restartRuntimeOnce)
141
+ const restartRuntime = createSerialRunner(restartRuntimeOnce)
379
142
 
380
- restartTail = run.then(
381
- () => true,
382
- () => false
383
- )
384
-
385
- return run
386
- }
387
-
388
- const requestRestart = (): void => {
389
- if (shuttingDown) return
390
-
391
- if (restarting) {
143
+ const restartRequester = createRestartRequester({
144
+ isShuttingDown: () => shuttingDown,
145
+ isRestarting: () => restarting,
146
+ markPending: () => {
392
147
  restartPending = true
393
- return
394
- }
395
-
396
- if (restartTimer !== null) return
148
+ },
149
+ restart: () => restartRuntime(),
150
+ })
397
151
 
398
- restartTimer = setTimeout(() => {
399
- restartTimer = null
400
- void restartRuntime()
401
- }, 250)
402
- }
152
+ const requestRestart = restartRequester.request
403
153
 
404
154
  const store = new ConfigStore({
405
155
  configPath,
@@ -407,98 +157,48 @@ async function main(): Promise<void> {
407
157
  restart: () => restartRuntime(),
408
158
  })
409
159
 
410
- const deps: WebUiDeps = {
411
- logger: logger.child('webui'),
412
- paths: {
413
- configPath,
414
- dataDir,
415
- pluginDir: initialSpec.pluginDir,
416
- staticDir: resolveStaticDir(),
417
- },
418
- version: readVersion(),
160
+ const runExclusive = createExclusiveRunner()
161
+
162
+ const deps = createWebUiDeps({
163
+ logger,
164
+ configPath,
165
+ dataDir,
166
+ pluginDir: initialSpec.pluginDir,
167
+ staticDir: resolveStaticDir(SCRIPT_DIR),
168
+ version: readVersion(SCRIPT_DIR),
419
169
  logs,
420
170
  events,
421
- getWebUiConfig: () => currentConfig.webui,
422
- readConfig: () => store.read(),
423
- validateConfig: (value) => store.validate(value),
424
- applyConfig: (value, baseHash) => store.apply(value, baseHash),
171
+ store,
172
+ getCurrentConfig: () => currentConfig,
173
+ getRuntime: () => runtime,
174
+ getGeneration: () => generation,
425
175
  restart: () => restartRuntime(),
426
176
  requestRestart,
427
- pause: () => runtime?.kernel.pause(),
428
- resume: () => runtime?.kernel.resume(),
429
177
  getListening: () => server.address,
430
178
  invalidateSessions: () => server.invalidateSessions(),
431
- runExclusive: async <T>(task: () => Promise<T>): Promise<T> => {
432
- const run = exclusiveTail.then(task, task)
433
-
434
- exclusiveTail = run.then(
435
- () => undefined,
436
- () => undefined
437
- )
438
-
439
- return run
440
- },
441
- getRuntime: () =>
442
- runtime === null
443
- ? null
444
- : {
445
- generation,
446
- config: runtime.spec.config,
447
- kernel: runtime.kernel,
448
- api: runtime.kernel.api,
449
- database: runtime.database,
450
- installer: runtime.installer,
451
- plugins: runtime.spec.plugins,
452
- },
179
+ runExclusive,
453
180
  isRestarting: () => restarting,
454
181
  getStartupError: () => startupError,
455
- getRequiresProcessRestart: () => {
456
- const webui = currentConfig.webui
457
-
458
- if (boundWebUi === null) return webui.enabled
459
-
460
- if (!webui.enabled) return true
461
-
462
- return (
463
- webui.host !== boundWebUi.host || webui.port !== boundWebUi.port
464
- )
465
- },
466
- }
182
+ getBoundWebUi: () => boundWebUi,
183
+ })
467
184
 
468
185
  const server = new WebUiServer({
469
186
  config: initialSpec.config.webui,
470
187
  deps,
471
188
  })
472
189
 
473
- const shutdown = async (signal: string, exitCode = 0): Promise<void> => {
474
- if (shuttingDown) return
475
-
476
- shuttingDown = true
477
-
478
- if (restartTimer !== null) {
479
- clearTimeout(restartTimer)
480
- restartTimer = null
481
- }
482
-
483
- logger.info(`received ${signal}, shutting down`)
484
-
485
- try {
486
- await runtime?.kernel.stop()
487
- await server.stop()
488
- process.exit(exitCode)
489
- } catch (error) {
490
- logger.error('shutdown failed', error)
491
- process.exit(1)
492
- }
493
- }
494
-
495
- process.once('SIGINT', () => {
496
- void shutdown('SIGINT')
190
+ const shutdown = createShutdown({
191
+ logger,
192
+ isShuttingDown: () => shuttingDown,
193
+ onBegin: () => {
194
+ shuttingDown = true
195
+ restartRequester.clear()
196
+ },
197
+ getRuntime: () => runtime,
198
+ stopServer: () => server.stop(),
497
199
  })
498
200
 
499
- process.once('SIGTERM', () => {
500
- void shutdown('SIGTERM')
501
- })
201
+ registerShutdownSignals(shutdown)
502
202
 
503
203
  // Start the WebUI before the kernel so it stays online when startup fails.
504
204
  if (initialSpec.config.webui.enabled) {
@@ -514,32 +214,24 @@ async function main(): Promise<void> {
514
214
  }
515
215
  }
516
216
 
517
- try {
518
- runtime = createRuntime(
519
- initialSpec,
520
- settings,
521
- requestRestart,
522
- tapEvents
523
- )
524
- await runtime.kernel.start()
525
-
526
- generation = 1
527
- startupError = null
528
- credentials = credentialSignature(currentConfig)
529
- logger.info('runtime started')
530
- } catch (error) {
531
- runtime = null
532
- startupError = describeError(error)
533
- logger.error('runtime startup failed', error)
534
-
535
- if (boundWebUi === null) {
536
- await shutdown('startup failure', 1)
537
- } else {
538
- logger.info(
539
- 'webui stays online; fix the configuration on the config page and restart the runtime'
540
- )
541
- }
542
- }
217
+ const started = await startInitialRuntime({
218
+ spec: initialSpec,
219
+ settings,
220
+ requestRestart,
221
+ onEvent: tapEvents,
222
+ getCurrentConfig: () => currentConfig,
223
+ hasWebUi: boundWebUi !== null,
224
+ onFailure: () => shutdown('startup failure', 1),
225
+ setRuntime: (value) => {
226
+ runtime = value
227
+ },
228
+ generation,
229
+ credentials,
230
+ })
231
+
232
+ generation = started.generation
233
+ startupError = started.startupError
234
+ credentials = started.credentials
543
235
  }
544
236
 
545
237
  await main().catch((error: unknown) => {
@@ -89,6 +89,12 @@ function uniqueStrings(values: string[]): string[] {
89
89
  return result
90
90
  }
91
91
 
92
+ const LEADING_REPLY = /^(?:\[CQ:reply(?:,[^\]]*)?\]\s*)+/
93
+
94
+ function stripLeadingReply(raw: string): string {
95
+ return raw.replace(LEADING_REPLY, '')
96
+ }
97
+
92
98
  function groupBases(name: string, aliases: string[]): string[] {
93
99
  return uniqueStrings([
94
100
  normalizeSegment('group name', name),
@@ -258,7 +264,7 @@ export class CommandRegistry {
258
264
  }
259
265
 
260
266
  match(rawMessage: string): RegisteredCommand | null {
261
- const raw = rawMessage.trim()
267
+ const raw = stripLeadingReply(rawMessage.trim()).trim()
262
268
 
263
269
  if (!raw) return null
264
270
 
@@ -195,7 +195,8 @@ export class Router {
195
195
  try {
196
196
  const parsed =
197
197
  found.registration.paramsParser.parse(
198
- found.match.rest
198
+ found.match.rest,
199
+ event
199
200
  )
200
201
 
201
202
  args = parsed.args