@jsbot-tellus/core 2.5.0 → 2.6.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 (175) 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/plugin/command-params.d.ts +3 -37
  13. package/dist/core/plugin/command-params.d.ts.map +1 -1
  14. package/dist/core/plugin/command-params.js +2 -580
  15. package/dist/core/plugin/command-params.js.map +1 -1
  16. package/dist/core/plugin/install/paths.d.ts +7 -0
  17. package/dist/core/plugin/install/paths.d.ts.map +1 -0
  18. package/dist/core/plugin/install/paths.js +49 -0
  19. package/dist/core/plugin/install/paths.js.map +1 -0
  20. package/dist/core/plugin/install/state.d.ts +23 -0
  21. package/dist/core/plugin/install/state.d.ts.map +1 -0
  22. package/dist/core/plugin/install/state.js +129 -0
  23. package/dist/core/plugin/install/state.js.map +1 -0
  24. package/dist/core/plugin/install/validate-script.d.ts +3 -0
  25. package/dist/core/plugin/install/validate-script.d.ts.map +1 -0
  26. package/dist/core/plugin/install/validate-script.js +154 -0
  27. package/dist/core/plugin/install/validate-script.js.map +1 -0
  28. package/dist/core/plugin/install-dir.d.ts +3 -28
  29. package/dist/core/plugin/install-dir.d.ts.map +1 -1
  30. package/dist/core/plugin/install-dir.js +3 -326
  31. package/dist/core/plugin/install-dir.js.map +1 -1
  32. package/dist/core/plugin/installer/checks.d.ts +6 -0
  33. package/dist/core/plugin/installer/checks.d.ts.map +1 -0
  34. package/dist/core/plugin/installer/checks.js +99 -0
  35. package/dist/core/plugin/installer/checks.js.map +1 -0
  36. package/dist/core/plugin/installer/support.d.ts +37 -0
  37. package/dist/core/plugin/installer/support.d.ts.map +1 -0
  38. package/dist/core/plugin/installer/support.js +67 -0
  39. package/dist/core/plugin/installer/support.js.map +1 -0
  40. package/dist/core/plugin/installer/warnings.d.ts +5 -0
  41. package/dist/core/plugin/installer/warnings.d.ts.map +1 -0
  42. package/dist/core/plugin/installer/warnings.js +28 -0
  43. package/dist/core/plugin/installer/warnings.js.map +1 -0
  44. package/dist/core/plugin/installer.d.ts +4 -30
  45. package/dist/core/plugin/installer.d.ts.map +1 -1
  46. package/dist/core/plugin/installer.js +21 -202
  47. package/dist/core/plugin/installer.js.map +1 -1
  48. package/dist/core/plugin/params/compile.d.ts +3 -0
  49. package/dist/core/plugin/params/compile.d.ts.map +1 -0
  50. package/dist/core/plugin/params/compile.js +23 -0
  51. package/dist/core/plugin/params/compile.js.map +1 -0
  52. package/dist/core/plugin/params/declaration.d.ts +9 -0
  53. package/dist/core/plugin/params/declaration.d.ts.map +1 -0
  54. package/dist/core/plugin/params/declaration.js +286 -0
  55. package/dist/core/plugin/params/declaration.js.map +1 -0
  56. package/dist/core/plugin/params/input.d.ts +5 -0
  57. package/dist/core/plugin/params/input.d.ts.map +1 -0
  58. package/dist/core/plugin/params/input.js +196 -0
  59. package/dist/core/plugin/params/input.js.map +1 -0
  60. package/dist/core/plugin/params/reply.d.ts +3 -0
  61. package/dist/core/plugin/params/reply.d.ts.map +1 -0
  62. package/dist/core/plugin/params/reply.js +49 -0
  63. package/dist/core/plugin/params/reply.js.map +1 -0
  64. package/dist/core/plugin/params/type-level.d.ts +24 -0
  65. package/dist/core/plugin/params/type-level.d.ts.map +1 -0
  66. package/dist/core/plugin/params/type-level.js +2 -0
  67. package/dist/core/plugin/params/type-level.js.map +1 -0
  68. package/dist/core/plugin/params/types.d.ts +33 -0
  69. package/dist/core/plugin/params/types.d.ts.map +1 -0
  70. package/dist/core/plugin/params/types.js +12 -0
  71. package/dist/core/plugin/params/types.js.map +1 -0
  72. package/dist/core/plugin/params/values.d.ts +7 -0
  73. package/dist/core/plugin/params/values.d.ts.map +1 -0
  74. package/dist/core/plugin/params/values.js +111 -0
  75. package/dist/core/plugin/params/values.js.map +1 -0
  76. package/dist/core/web/api.d.ts +2 -14
  77. package/dist/core/web/api.d.ts.map +1 -1
  78. package/dist/core/web/api.js +8 -500
  79. package/dist/core/web/api.js.map +1 -1
  80. package/dist/core/web/routes/actions.d.ts +4 -0
  81. package/dist/core/web/routes/actions.d.ts.map +1 -0
  82. package/dist/core/web/routes/actions.js +33 -0
  83. package/dist/core/web/routes/actions.js.map +1 -0
  84. package/dist/core/web/routes/config.d.ts +5 -0
  85. package/dist/core/web/routes/config.d.ts.map +1 -0
  86. package/dist/core/web/routes/config.js +30 -0
  87. package/dist/core/web/routes/config.js.map +1 -0
  88. package/dist/core/web/routes/kv.d.ts +6 -0
  89. package/dist/core/web/routes/kv.d.ts.map +1 -0
  90. package/dist/core/web/routes/kv.js +80 -0
  91. package/dist/core/web/routes/kv.js.map +1 -0
  92. package/dist/core/web/routes/plugins.d.ts +5 -0
  93. package/dist/core/web/routes/plugins.d.ts.map +1 -0
  94. package/dist/core/web/routes/plugins.js +98 -0
  95. package/dist/core/web/routes/plugins.js.map +1 -0
  96. package/dist/core/web/routes/sessions.d.ts +8 -0
  97. package/dist/core/web/routes/sessions.d.ts.map +1 -0
  98. package/dist/core/web/routes/sessions.js +130 -0
  99. package/dist/core/web/routes/sessions.js.map +1 -0
  100. package/dist/core/web/routes/shared.d.ts +18 -0
  101. package/dist/core/web/routes/shared.d.ts.map +1 -0
  102. package/dist/core/web/routes/shared.js +19 -0
  103. package/dist/core/web/routes/shared.js.map +1 -0
  104. package/dist/core/web/routes/status.d.ts +6 -0
  105. package/dist/core/web/routes/status.d.ts.map +1 -0
  106. package/dist/core/web/routes/status.js +132 -0
  107. package/dist/core/web/routes/status.js.map +1 -0
  108. package/dist/core/web/server/auth.d.ts +17 -0
  109. package/dist/core/web/server/auth.d.ts.map +1 -0
  110. package/dist/core/web/server/auth.js +68 -0
  111. package/dist/core/web/server/auth.js.map +1 -0
  112. package/dist/core/web/server/csp.d.ts +11 -0
  113. package/dist/core/web/server/csp.d.ts.map +1 -0
  114. package/dist/core/web/server/csp.js +31 -0
  115. package/dist/core/web/server/csp.js.map +1 -0
  116. package/dist/core/web/server/request.d.ts +6 -0
  117. package/dist/core/web/server/request.d.ts.map +1 -0
  118. package/dist/core/web/server/request.js +33 -0
  119. package/dist/core/web/server/request.js.map +1 -0
  120. package/dist/core/web/server/security.d.ts +4 -0
  121. package/dist/core/web/server/security.d.ts.map +1 -0
  122. package/dist/core/web/server/security.js +12 -0
  123. package/dist/core/web/server/security.js.map +1 -0
  124. package/dist/core/web/server/sse.d.ts +18 -0
  125. package/dist/core/web/server/sse.d.ts.map +1 -0
  126. package/dist/core/web/server/sse.js +49 -0
  127. package/dist/core/web/server/sse.js.map +1 -0
  128. package/dist/core/web/server/static.d.ts +16 -0
  129. package/dist/core/web/server/static.d.ts.map +1 -0
  130. package/dist/core/web/server/static.js +80 -0
  131. package/dist/core/web/server/static.js.map +1 -0
  132. package/dist/core/web/server.d.ts +0 -7
  133. package/dist/core/web/server.d.ts.map +1 -1
  134. package/dist/core/web/server.js +41 -263
  135. package/dist/core/web/server.js.map +1 -1
  136. package/docs/architecture.md +28 -11
  137. package/docs/configuration.md +1 -1
  138. package/docs/migration.md +2 -2
  139. package/docs/plugin-api.md +24 -11
  140. package/docs/plugin-development.md +1 -1
  141. package/package.json +19 -17
  142. package/src/app/cli.ts +47 -0
  143. package/src/app/runtime.ts +522 -0
  144. package/src/app.ts +87 -395
  145. package/src/core/plugin/command-params.ts +8 -871
  146. package/src/core/plugin/install/paths.ts +72 -0
  147. package/src/core/plugin/install/state.ts +196 -0
  148. package/src/core/plugin/install/validate-script.ts +154 -0
  149. package/src/core/plugin/install-dir.ts +19 -427
  150. package/src/core/plugin/installer/checks.ts +137 -0
  151. package/src/core/plugin/installer/support.ts +120 -0
  152. package/src/core/plugin/installer/warnings.ts +52 -0
  153. package/src/core/plugin/installer.ts +52 -301
  154. package/src/core/plugin/params/compile.ts +41 -0
  155. package/src/core/plugin/params/declaration.ts +394 -0
  156. package/src/core/plugin/params/input.ts +281 -0
  157. package/src/core/plugin/params/reply.ts +59 -0
  158. package/src/core/plugin/params/type-level.ts +75 -0
  159. package/src/core/plugin/params/types.ts +55 -0
  160. package/src/core/plugin/params/values.ts +150 -0
  161. package/src/core/web/api.ts +33 -759
  162. package/src/core/web/routes/actions.ts +43 -0
  163. package/src/core/web/routes/config.ts +42 -0
  164. package/src/core/web/routes/kv.ts +122 -0
  165. package/src/core/web/routes/plugins.ts +151 -0
  166. package/src/core/web/routes/sessions.ts +188 -0
  167. package/src/core/web/routes/shared.ts +42 -0
  168. package/src/core/web/routes/status.ts +191 -0
  169. package/src/core/web/server/auth.ts +126 -0
  170. package/src/core/web/server/csp.ts +33 -0
  171. package/src/core/web/server/request.ts +38 -0
  172. package/src/core/web/server/security.ts +14 -0
  173. package/src/core/web/server/sse.ts +74 -0
  174. package/src/core/web/server/static.ts +112 -0
  175. package/src/core/web/server.ts +41 -383
package/src/app/cli.ts ADDED
@@ -0,0 +1,47 @@
1
+ import { parseArgs } from 'node:util'
2
+ import { LOCAL_PLUGIN_DIR } from './runtime.js'
3
+
4
+ export const USAGE = `Usage: jsbot [options]
5
+
6
+ Options:
7
+ --config <path> Config file path (env: BOT_CONFIG)
8
+ --data-dir <path> Data directory (env: BOT_DATA_DIR)
9
+ --log-level <level> debug, info, warn or error (env: BOT_LOG_LEVEL)
10
+ --help Show this help
11
+
12
+ Config precedence: CLI > environment > ./bot.config.json.
13
+ Data precedence: CLI > environment > ./data.
14
+ Local plugins: ./${LOCAL_PLUGIN_DIR}/<id>/dist (auto-built when missing).
15
+ `
16
+
17
+ export function pick(value: unknown): string | undefined {
18
+ return typeof value === 'string' && value.trim().length > 0
19
+ ? value
20
+ : undefined
21
+ }
22
+
23
+ export interface CliOptions {
24
+ help: boolean
25
+ config: string | undefined
26
+ dataDir: string | undefined
27
+ logLevel: string | undefined
28
+ }
29
+
30
+ export function parseCli(): CliOptions {
31
+ const { values } = parseArgs({
32
+ options: {
33
+ config: { type: 'string' },
34
+ 'data-dir': { type: 'string' },
35
+ 'log-level': { type: 'string' },
36
+ help: { type: 'boolean', short: 'h' },
37
+ },
38
+ allowPositionals: false,
39
+ })
40
+
41
+ return {
42
+ help: values.help === true,
43
+ config: values.config,
44
+ dataDir: values['data-dir'],
45
+ logLevel: values['log-level'],
46
+ }
47
+ }
@@ -0,0 +1,522 @@
1
+ import type { SnowLumaEvent } from '@snowluma/sdk'
2
+ import { existsSync, readFileSync } from 'node:fs'
3
+ import { join, resolve } from 'node:path'
4
+ import type { AppConfig } from '../core/config/schema.js'
5
+ import {
6
+ loadConfig,
7
+ resolveConfigPath,
8
+ resolveDataDir,
9
+ } from '../core/config/loader.js'
10
+ import { createBotClient } from '../core/client.js'
11
+ import { Kernel } from '../core/kernel.js'
12
+ import { LogBuffer } from '../core/logging/log-buffer.js'
13
+ import {
14
+ createLogger,
15
+ defaultLogSink,
16
+ parseLogLevel,
17
+ type LogSink,
18
+ type Logger,
19
+ } from '../core/logging/logger.js'
20
+ import { NpmPluginInstaller } from '../core/plugin/installer.js'
21
+ import { loadLocalPlugins } from '../core/plugin/loader.js'
22
+ import type {
23
+ AnyPluginDefinition,
24
+ PluginInstaller,
25
+ } from '../core/plugin/types.js'
26
+ import { openDatabase, type Database } from '../core/storage/database.js'
27
+ import { ConfigStore } from '../core/web/config-store.js'
28
+ import { EventFeed } from '../core/web/event-feed.js'
29
+ import type {
30
+ WebUiAddress,
31
+ WebUiDeps,
32
+ WebUiRuntime,
33
+ } from '../core/web/types.js'
34
+ import { builtinPlugins } from '../plugins/index.js'
35
+
36
+ export const LOCAL_PLUGIN_DIR = 'plugins'
37
+
38
+ export interface PluginSource {
39
+ label: string
40
+ plugins: AnyPluginDefinition[]
41
+ }
42
+
43
+ export interface RuntimeSettings {
44
+ logger: Logger
45
+ configPath: string
46
+ dataDir: string
47
+ }
48
+
49
+ export interface RuntimeSpec {
50
+ config: AppConfig
51
+ dataDir: string
52
+ pluginDir: string
53
+ plugins: AnyPluginDefinition[]
54
+ }
55
+
56
+ export interface Runtime {
57
+ spec: RuntimeSpec
58
+ kernel: Kernel
59
+ database: Database
60
+ installer: PluginInstaller
61
+ }
62
+
63
+ export interface StaticBinding {
64
+ host: string
65
+ port: number
66
+ }
67
+
68
+ export interface AppLogging {
69
+ logs: LogBuffer
70
+ events: EventFeed
71
+ logger: Logger
72
+ tapEvents: (event: SnowLumaEvent) => void
73
+ }
74
+
75
+ export function createAppLogging(logLevel: string | undefined): AppLogging {
76
+ const logs = new LogBuffer()
77
+ const events = new EventFeed()
78
+ const tapEvents = (event: SnowLumaEvent): void => {
79
+ events.add(event)
80
+ }
81
+ const sink: LogSink = (record) => {
82
+ defaultLogSink(record)
83
+ logs.push(record)
84
+ }
85
+
86
+ const logger = createLogger({
87
+ level: parseLogLevel(logLevel),
88
+ scope: 'app',
89
+ sink,
90
+ })
91
+
92
+ return { logs, events, logger, tapEvents }
93
+ }
94
+
95
+ export interface ResolveRuntimeSettingsOptions {
96
+ config: string | undefined
97
+ dataDir: string | undefined
98
+ env: NodeJS.ProcessEnv
99
+ }
100
+
101
+ export function resolveRuntimeSettings(
102
+ logger: Logger,
103
+ options: ResolveRuntimeSettingsOptions
104
+ ): RuntimeSettings {
105
+ const configPath = resolveConfigPath({
106
+ config: options.config,
107
+ env: options.env,
108
+ })
109
+
110
+ const dataDir = resolveDataDir({
111
+ dataDir: options.dataDir,
112
+ env: options.env,
113
+ })
114
+
115
+ return {
116
+ logger,
117
+ configPath,
118
+ dataDir,
119
+ }
120
+ }
121
+
122
+ export function mergePlugins(
123
+ sources: PluginSource[],
124
+ logger: Logger
125
+ ): AnyPluginDefinition[] {
126
+ const seen = new Set<string>()
127
+ const plugins: AnyPluginDefinition[] = []
128
+
129
+ for (const source of sources) {
130
+ for (const plugin of source.plugins) {
131
+ const id = plugin.manifest.id
132
+
133
+ if (seen.has(id)) {
134
+ logger.warn(
135
+ `skipping ${source.label} plugin ${id}: plugin id already loaded`
136
+ )
137
+ continue
138
+ }
139
+
140
+ seen.add(id)
141
+ plugins.push(plugin)
142
+ }
143
+ }
144
+
145
+ return plugins
146
+ }
147
+
148
+ export async function loadRuntimeSpec(
149
+ settings: RuntimeSettings
150
+ ): Promise<RuntimeSpec> {
151
+ const { logger } = settings
152
+
153
+ logger.info('loading config', settings.configPath)
154
+
155
+ const config = await loadConfig(settings.configPath)
156
+ const pluginDir = resolve(LOCAL_PLUGIN_DIR)
157
+
158
+ logger.info('loading plugins from', pluginDir)
159
+
160
+ const localPlugins = await loadLocalPlugins(pluginDir, logger, {
161
+ buildTimeoutMs: config.installer.timeoutMs,
162
+ })
163
+
164
+ if (localPlugins.length > 0) {
165
+ logger.info(`loaded ${localPlugins.length} plugin(s) from ${pluginDir}`)
166
+ }
167
+
168
+ const plugins = mergePlugins(
169
+ [
170
+ { label: 'built-in', plugins: builtinPlugins },
171
+ { label: 'plugins', plugins: localPlugins },
172
+ ],
173
+ logger
174
+ )
175
+
176
+ return {
177
+ config,
178
+ dataDir: settings.dataDir,
179
+ pluginDir,
180
+ plugins,
181
+ }
182
+ }
183
+
184
+ export function createRuntime(
185
+ spec: RuntimeSpec,
186
+ settings: RuntimeSettings,
187
+ onRestart: () => void,
188
+ onEvent: (event: SnowLumaEvent) => void
189
+ ): Runtime {
190
+ const { logger } = settings
191
+ const installer = new NpmPluginInstaller({
192
+ dir: spec.pluginDir,
193
+ config: spec.config.installer,
194
+ logger: logger.child('installer'),
195
+ reservedIds: () =>
196
+ new Set(spec.plugins.map((plugin) => plugin.manifest.id)),
197
+ configuredPluginIds: () => new Set(Object.keys(spec.config.plugins)),
198
+ })
199
+ const database = openDatabase({ dataDir: spec.dataDir })
200
+
201
+ const client = createBotClient({
202
+ url: spec.config.url,
203
+ ...(spec.config.accessToken
204
+ ? { accessToken: spec.config.accessToken }
205
+ : {}),
206
+ })
207
+
208
+ const kernel = new Kernel({
209
+ config: spec.config,
210
+ client,
211
+ logger,
212
+ database,
213
+ plugins: spec.plugins,
214
+ installer,
215
+ onRestart,
216
+ onEvent,
217
+ })
218
+
219
+ return { spec, kernel, database, installer }
220
+ }
221
+
222
+ export function describeError(error: unknown): string {
223
+ if (error instanceof Error) {
224
+ return error.stack ?? `${error.name}: ${error.message}`
225
+ }
226
+
227
+ return String(error)
228
+ }
229
+
230
+ export function credentialSignature(config: AppConfig): string {
231
+ const webui = config.webui
232
+
233
+ return [webui.username ?? '', webui.password ?? ''].join(':')
234
+ }
235
+
236
+ export function resolveStaticDir(scriptDir: string): string | null {
237
+ const candidates = [
238
+ join(scriptDir, 'webui'),
239
+ resolve(process.cwd(), 'dist/webui'),
240
+ ]
241
+
242
+ for (const candidate of candidates) {
243
+ if (existsSync(join(candidate, 'index.html'))) return candidate
244
+ }
245
+
246
+ return null
247
+ }
248
+
249
+ export function readVersion(scriptDir: string): string {
250
+ try {
251
+ const pkgPath = join(scriptDir, '..', 'package.json')
252
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8')) as {
253
+ version?: string
254
+ }
255
+
256
+ return pkg.version ?? 'unknown'
257
+ } catch {
258
+ return 'unknown'
259
+ }
260
+ }
261
+
262
+ export function createExclusiveRunner(): <T>(
263
+ task: () => Promise<T>
264
+ ) => Promise<T> {
265
+ let tail: Promise<unknown> = Promise.resolve()
266
+
267
+ return <T>(task: () => Promise<T>): Promise<T> => {
268
+ const run = tail.then(task, task)
269
+
270
+ tail = run.then(
271
+ () => undefined,
272
+ () => undefined
273
+ )
274
+
275
+ return run
276
+ }
277
+ }
278
+
279
+ export function restartDrainTimeout(previous: Runtime | null): number {
280
+ const installTimeout = previous?.spec.config.installer.timeoutMs ?? 5_000
281
+
282
+ return Math.max(installTimeout, 5_000) * 2 + 5_000
283
+ }
284
+
285
+ export interface StartInitialRuntimeOptions {
286
+ spec: RuntimeSpec
287
+ settings: RuntimeSettings
288
+ requestRestart: () => void
289
+ onEvent: (event: SnowLumaEvent) => void
290
+ getCurrentConfig: () => AppConfig
291
+ hasWebUi: boolean
292
+ onFailure: () => Promise<void>
293
+ setRuntime: (runtime: Runtime | null) => void
294
+ generation: number
295
+ credentials: string
296
+ }
297
+
298
+ export interface InitialRuntimeResult {
299
+ generation: number
300
+ startupError: string | null
301
+ credentials: string
302
+ }
303
+
304
+ export async function startInitialRuntime(
305
+ options: StartInitialRuntimeOptions
306
+ ): Promise<InitialRuntimeResult> {
307
+ const { spec, settings, requestRestart, onEvent, getCurrentConfig } =
308
+ options
309
+ const { logger } = settings
310
+
311
+ try {
312
+ const runtime = createRuntime(spec, settings, requestRestart, onEvent)
313
+
314
+ options.setRuntime(runtime)
315
+
316
+ await runtime.kernel.start()
317
+
318
+ const credentials = credentialSignature(getCurrentConfig())
319
+
320
+ logger.info('runtime started')
321
+
322
+ return {
323
+ generation: 1,
324
+ startupError: null,
325
+ credentials,
326
+ }
327
+ } catch (error) {
328
+ options.setRuntime(null)
329
+
330
+ logger.error('runtime startup failed', error)
331
+
332
+ if (!options.hasWebUi) {
333
+ await options.onFailure()
334
+ } else {
335
+ logger.info(
336
+ 'webui stays online; fix the configuration on the config page and restart the runtime'
337
+ )
338
+ }
339
+
340
+ return {
341
+ generation: options.generation,
342
+ startupError: describeError(error),
343
+ credentials: options.credentials,
344
+ }
345
+ }
346
+ }
347
+
348
+ export interface ShutdownOptions {
349
+ logger: Logger
350
+ isShuttingDown: () => boolean
351
+ onBegin: () => void
352
+ getRuntime: () => Runtime | null
353
+ stopServer: () => Promise<void>
354
+ }
355
+
356
+ export function createShutdown(
357
+ options: ShutdownOptions
358
+ ): (signal: string, exitCode?: number) => Promise<void> {
359
+ return async (signal, exitCode = 0) => {
360
+ if (options.isShuttingDown()) return
361
+
362
+ options.onBegin()
363
+
364
+ options.logger.info(`received ${signal}, shutting down`)
365
+
366
+ try {
367
+ await options.getRuntime()?.kernel.stop()
368
+ await options.stopServer()
369
+ process.exit(exitCode)
370
+ } catch (error) {
371
+ options.logger.error('shutdown failed', error)
372
+ process.exit(1)
373
+ }
374
+ }
375
+ }
376
+
377
+ export function registerShutdownSignals(
378
+ shutdown: (signal: string, exitCode?: number) => Promise<void>
379
+ ): void {
380
+ process.once('SIGINT', () => {
381
+ void shutdown('SIGINT')
382
+ })
383
+
384
+ process.once('SIGTERM', () => {
385
+ void shutdown('SIGTERM')
386
+ })
387
+ }
388
+
389
+ export function createSerialRunner<T>(run: () => Promise<T>): () => Promise<T> {
390
+ let tail: Promise<unknown> = Promise.resolve()
391
+
392
+ return () => {
393
+ const result = tail.then(run, run)
394
+
395
+ tail = result.then(
396
+ () => undefined,
397
+ () => undefined
398
+ )
399
+
400
+ return result
401
+ }
402
+ }
403
+
404
+ export interface RestartRequesterOptions {
405
+ isShuttingDown: () => boolean
406
+ isRestarting: () => boolean
407
+ markPending: () => void
408
+ restart: () => Promise<boolean>
409
+ }
410
+
411
+ export interface RestartRequester {
412
+ request(): void
413
+ clear(): void
414
+ }
415
+
416
+ export function createRestartRequester(
417
+ options: RestartRequesterOptions
418
+ ): RestartRequester {
419
+ let timer: NodeJS.Timeout | null = null
420
+
421
+ return {
422
+ request: () => {
423
+ if (options.isShuttingDown()) return
424
+
425
+ if (options.isRestarting()) {
426
+ options.markPending()
427
+ return
428
+ }
429
+
430
+ if (timer !== null) return
431
+
432
+ timer = setTimeout(() => {
433
+ timer = null
434
+ void options.restart()
435
+ }, 250)
436
+ },
437
+ clear: () => {
438
+ if (timer === null) return
439
+
440
+ clearTimeout(timer)
441
+ timer = null
442
+ },
443
+ }
444
+ }
445
+
446
+ export interface WebUiDepsOptions {
447
+ logger: Logger
448
+ configPath: string
449
+ dataDir: string
450
+ pluginDir: string
451
+ staticDir: string | null
452
+ version: string
453
+ logs: LogBuffer
454
+ events: EventFeed
455
+ store: ConfigStore
456
+ getCurrentConfig: () => AppConfig
457
+ getRuntime: () => Runtime | null
458
+ getGeneration: () => number
459
+ restart: () => Promise<boolean>
460
+ requestRestart: () => void
461
+ getListening: () => WebUiAddress | null
462
+ invalidateSessions: () => void
463
+ runExclusive: <T>(task: () => Promise<T>) => Promise<T>
464
+ isRestarting: () => boolean
465
+ getStartupError: () => string | null
466
+ getBoundWebUi: () => StaticBinding | null
467
+ }
468
+
469
+ export function createWebUiDeps(options: WebUiDepsOptions): WebUiDeps {
470
+ return {
471
+ logger: options.logger.child('webui'),
472
+ paths: {
473
+ configPath: options.configPath,
474
+ dataDir: options.dataDir,
475
+ pluginDir: options.pluginDir,
476
+ staticDir: options.staticDir,
477
+ },
478
+ version: options.version,
479
+ logs: options.logs,
480
+ events: options.events,
481
+ getWebUiConfig: () => options.getCurrentConfig().webui,
482
+ readConfig: () => options.store.read(),
483
+ validateConfig: (value) => options.store.validate(value),
484
+ applyConfig: (value, baseHash) => options.store.apply(value, baseHash),
485
+ restart: () => options.restart(),
486
+ requestRestart: options.requestRestart,
487
+ pause: () => options.getRuntime()?.kernel.pause(),
488
+ resume: () => options.getRuntime()?.kernel.resume(),
489
+ getListening: () => options.getListening(),
490
+ invalidateSessions: () => options.invalidateSessions(),
491
+ runExclusive: (task) => options.runExclusive(task),
492
+ getRuntime: (): WebUiRuntime | null => {
493
+ const runtime = options.getRuntime()
494
+
495
+ return runtime === null
496
+ ? null
497
+ : {
498
+ generation: options.getGeneration(),
499
+ config: runtime.spec.config,
500
+ kernel: runtime.kernel,
501
+ api: runtime.kernel.api,
502
+ database: runtime.database,
503
+ installer: runtime.installer,
504
+ plugins: runtime.spec.plugins,
505
+ }
506
+ },
507
+ isRestarting: () => options.isRestarting(),
508
+ getStartupError: () => options.getStartupError(),
509
+ getRequiresProcessRestart: () => {
510
+ const webui = options.getCurrentConfig().webui
511
+ const boundWebUi = options.getBoundWebUi()
512
+
513
+ if (boundWebUi === null) return webui.enabled
514
+
515
+ if (!webui.enabled) return true
516
+
517
+ return (
518
+ webui.host !== boundWebUi.host || webui.port !== boundWebUi.port
519
+ )
520
+ },
521
+ }
522
+ }