@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.
- 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/plugin/command-params.d.ts +3 -37
- package/dist/core/plugin/command-params.d.ts.map +1 -1
- package/dist/core/plugin/command-params.js +2 -580
- 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 +286 -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 +196 -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 +24 -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 +33 -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 +7 -0
- package/dist/core/plugin/params/values.d.ts.map +1 -0
- package/dist/core/plugin/params/values.js +111 -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 +24 -11
- 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/plugin/command-params.ts +8 -871
- 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 +394 -0
- package/src/core/plugin/params/input.ts +281 -0
- package/src/core/plugin/params/reply.ts +59 -0
- package/src/core/plugin/params/type-level.ts +75 -0
- package/src/core/plugin/params/types.ts +55 -0
- package/src/core/plugin/params/values.ts +150 -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
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
|
+
}
|