dsh-dbhub-live 3.0.0 → 3.1.2
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/AGENTS.md +21 -10
- package/README.en.md +162 -0
- package/README.md +56 -28
- package/doc/REQUIREMENTS.md +23 -3
- package/lib/adhoc.mjs +96 -80
- package/lib/client.js +498 -71
- package/lib/config.mjs +142 -16
- package/lib/i18n.mjs +139 -0
- package/lib/index.mjs +165 -37
- package/lib/mcp.mjs +114 -128
- package/lib/options.mjs +94 -0
- package/lib/runtime.mjs +14 -16
- package/lib/tools.mjs +461 -303
- package/package.json +6 -5
- package/test/adhoc.test.mjs +29 -0
- package/test/client-format.test.mjs +49 -1
- package/test/i18n.test.mjs +27 -0
- package/test/init.test.mjs +162 -105
- package/test/options.test.mjs +41 -0
package/lib/mcp.mjs
CHANGED
|
@@ -9,13 +9,16 @@
|
|
|
9
9
|
// disposers (the source of truth for the registered-tool count).
|
|
10
10
|
|
|
11
11
|
import { statSync } from 'node:fs'
|
|
12
|
-
import { join } from 'node:path'
|
|
12
|
+
import { join, basename } from 'node:path'
|
|
13
13
|
import {
|
|
14
|
-
DATA_DIR, dshHomeDir, STORE_PATH, TOML_PATH,
|
|
15
|
-
store, listWorkspaces,
|
|
14
|
+
DATA_DIR, dshHomeDir, STORE_PATH, TOML_PATH,
|
|
15
|
+
store, listWorkspaces, listWorkspaceEnvironments, resolveWorkspaceEnvs,
|
|
16
|
+
slugify, envSlug, shortHash,
|
|
16
17
|
fingerprintOf, generateToml, maskDsn, writeToml,
|
|
17
18
|
} from './config.mjs'
|
|
18
19
|
import { buildSpawnArgv, resolveDbhubExe } from './runtime.mjs'
|
|
20
|
+
import { currentT } from './i18n.mjs'
|
|
21
|
+
import * as options from './options.mjs'
|
|
19
22
|
import * as state from './state.mjs'
|
|
20
23
|
|
|
21
24
|
// ── MCP client (JSON-RPC 2.0 over stdio) ─────────────────────────────────
|
|
@@ -188,120 +191,48 @@ async function spawnServer(subprocess, signal) {
|
|
|
188
191
|
return {}
|
|
189
192
|
}
|
|
190
193
|
|
|
191
|
-
// ──
|
|
192
|
-
|
|
193
|
-
//
|
|
194
|
-
|
|
195
|
-
|
|
194
|
+
// ── tool registry (single-slot) ────────────────────────────────────────────
|
|
195
|
+
//
|
|
196
|
+
// Context budget: there is exactly ONE set of dbhub tools, period. Work
|
|
197
|
+
// workspaces × environments are NOT expanded into per-source tools (that
|
|
198
|
+
// would inject 2N tool declarations into every model turn); instead the
|
|
199
|
+
// source-parameterized tools dbhub_execute_sql / dbhub_search_objects resolve
|
|
200
|
+
// their `source` argument at call time against the live source list.
|
|
201
|
+
const toolDisposers = new Map() // modelName -> disposer (plugin-owned tools)
|
|
202
|
+
const coreDisposers = toolDisposers
|
|
196
203
|
|
|
197
|
-
/** Total registered dbhub tools (
|
|
204
|
+
/** Total registered dbhub tools (constant set: configure/list/execute/search/query/query_objects). */
|
|
198
205
|
export function activeToolCount() {
|
|
199
|
-
return coreDisposers.size
|
|
206
|
+
return coreDisposers.size
|
|
200
207
|
}
|
|
201
208
|
|
|
202
209
|
function refreshToolCount() {
|
|
203
210
|
state.setToolCount(activeToolCount())
|
|
204
211
|
}
|
|
205
212
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
if (!raw || typeof raw !== 'string') continue
|
|
223
|
-
// split off a per-source slug suffix (dbhub suffix = our source id)
|
|
224
|
-
let base = raw
|
|
225
|
-
let slug
|
|
226
|
-
for (const id of sourceById.keys()) {
|
|
227
|
-
if (raw === id) { slug = id } else if (raw.endsWith('_' + id)) {
|
|
228
|
-
base = raw.slice(0, raw.length - id.length - 1)
|
|
229
|
-
slug = id
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
if (slug === undefined && sources.length === 1) {
|
|
233
|
-
// single source: dbhub emits bare names -> attribute to the only source
|
|
234
|
-
slug = sources[0].id
|
|
235
|
-
}
|
|
236
|
-
if (slug === undefined) continue
|
|
237
|
-
const src = sourceById.get(slug)
|
|
238
|
-
const dsnMasked = src ? maskDsn(src.dsn) : slug
|
|
239
|
-
if (!bases.has(base)) bases.set(base, new Map())
|
|
240
|
-
bases.get(base).set(slug, { rawName: raw, dsnMasked })
|
|
241
|
-
}
|
|
242
|
-
const seen = new Set()
|
|
243
|
-
const jobs = []
|
|
244
|
-
const inputByRaw = new Map(tools.map((t) => [t.name, t.inputSchema]))
|
|
245
|
-
const descByRaw = new Map(tools.map((t) => [t.name, t.description]))
|
|
246
|
-
for (const [base, bySlug] of bases) {
|
|
247
|
-
for (const [slug, info] of bySlug) {
|
|
248
|
-
const modelName = 'dbhub_' + base + '_' + slug
|
|
249
|
-
seen.add(modelName)
|
|
250
|
-
if (toolDisposers.has(modelName)) continue
|
|
251
|
-
const rawName = info.rawName
|
|
252
|
-
const dsnMasked = info.dsnMasked
|
|
253
|
-
const inputSchema = inputByRaw.get(rawName)
|
|
254
|
-
const desc = descByRaw.get(rawName)
|
|
255
|
-
jobs.push(
|
|
256
|
-
(async () => {
|
|
257
|
-
try {
|
|
258
|
-
const def = {
|
|
259
|
-
name: modelName,
|
|
260
|
-
description: (desc ? desc + ' ' : '') +
|
|
261
|
-
'(连接源:' + slug + ' → ' + dsnMasked + ';注意:这是该工作区配置的数据库环境,请确认是你要查的那套)',
|
|
262
|
-
parameters: (inputSchema && typeof inputSchema === 'object' && inputSchema.type === 'object')
|
|
263
|
-
? inputSchema
|
|
264
|
-
: { type: 'object', properties: {}, required: [] },
|
|
265
|
-
timeoutMs: 60000,
|
|
266
|
-
output: {
|
|
267
|
-
schema: {},
|
|
268
|
-
render: (_args, value) => [
|
|
269
|
-
{
|
|
270
|
-
type: 'text',
|
|
271
|
-
text: String(value && value.text !== undefined ? value.text : JSON.stringify(value)),
|
|
272
|
-
},
|
|
273
|
-
],
|
|
274
|
-
},
|
|
275
|
-
async execute(args, exec) {
|
|
276
|
-
return runOnServer(ctx, subprocess, rawName, args, exec, '{连接: ' + slug + ' → ' + dsnMasked + '}')
|
|
277
|
-
},
|
|
278
|
-
}
|
|
279
|
-
const disposer = ctx.tools.register(def)
|
|
280
|
-
toolDisposers.set(modelName, disposer)
|
|
281
|
-
} catch (e) {
|
|
282
|
-
console.error('[dsh-dbhub-live] register failed for ' + modelName + ': ' + String((e && e.message) || e))
|
|
283
|
-
}
|
|
284
|
-
})(),
|
|
285
|
-
)
|
|
286
|
-
}
|
|
287
|
-
}
|
|
288
|
-
// unregister tools whose source vanished
|
|
289
|
-
for (const [modelName, disposer] of [...toolDisposers]) {
|
|
290
|
-
if (!seen.has(modelName)) {
|
|
291
|
-
try {
|
|
292
|
-
disposer()
|
|
293
|
-
} catch (e) {
|
|
294
|
-
/* ignore */
|
|
295
|
-
}
|
|
296
|
-
toolDisposers.delete(modelName)
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
return Promise.all(jobs).then(() => {
|
|
300
|
-
refreshToolCount()
|
|
301
|
-
})
|
|
302
|
-
}).catch((e) => {
|
|
303
|
-
console.error('[dsh-dbhub-live] tools/list failed: ' + String((e && e.message) || e))
|
|
213
|
+
/**
|
|
214
|
+
* Resolve a `source` reference (from dbhub_list_sources) to a live source row.
|
|
215
|
+
* Accepts: the exact source id (`<title>_<hash>[_<env>]`), the display titles
|
|
216
|
+
* `<title>` / `<title> <env>` / `<title>_<env>` / `<title>.<env>`.
|
|
217
|
+
* @returns the matched {@link collectSources} row + its source id, or
|
|
218
|
+
* `undefined` when nothing matches.
|
|
219
|
+
*/
|
|
220
|
+
export async function resolveSource(ctx, subprocess, sourceRef) {
|
|
221
|
+
const wanted = String(sourceRef || '').trim().toLowerCase()
|
|
222
|
+
if (!wanted) return undefined
|
|
223
|
+
const { rows } = await collectSources(ctx, subprocess)
|
|
224
|
+
const keyed = rows.map((r, i) => {
|
|
225
|
+
const title = slugify(r.title)
|
|
226
|
+
const env = r.env === 'default' ? '' : envSlug(r.env)
|
|
227
|
+
const id = title + '_' + shortHash(r.wsPath) + (env ? '_' + env : '')
|
|
228
|
+
return { row: r, id, keys: new Set([id, title, env ? title + '_' + env : title, env ? title + ' ' + r.env : title, env ? title + '.' + r.env : title, String(i)]) }
|
|
304
229
|
})
|
|
230
|
+
return keyed.find((k) => k.keys.has(wanted) || k.id === wanted || (wanted.includes(k.id)))
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Human label for a source row ({连接: <title> → masked dsn}). */
|
|
234
|
+
export function sourceLabel(row) {
|
|
235
|
+
return row ? '{连接: ' + row.title + ' → ' + maskDsn(row.dsn) + '}' : ''
|
|
305
236
|
}
|
|
306
237
|
|
|
307
238
|
// ── warm-call fast path & lazy ensure ─────────────────────────────────────
|
|
@@ -317,11 +248,12 @@ function resetIdle(ctx) {
|
|
|
317
248
|
idleDispose = undefined
|
|
318
249
|
}
|
|
319
250
|
if (!server.handle) return
|
|
251
|
+
const idleMs = (options.get('idleMinutes') || 10) * 60 * 1000
|
|
320
252
|
idleDispose = ctx.timeout(() => {
|
|
321
253
|
idleDispose = undefined
|
|
322
254
|
console.log('[dsh-dbhub-live] idle — recycling dbhub process')
|
|
323
255
|
terminateServer()
|
|
324
|
-
},
|
|
256
|
+
}, idleMs)
|
|
325
257
|
}
|
|
326
258
|
|
|
327
259
|
// Re-resolve configs only when the durable inputs change (workspace registry
|
|
@@ -329,7 +261,33 @@ function resetIdle(ctx) {
|
|
|
329
261
|
// Keeps repeated tool calls at ~0 overhead.
|
|
330
262
|
let lastInputMtime = 0
|
|
331
263
|
|
|
332
|
-
|
|
264
|
+
// Live connection-row cache: refreshed by every collectSources run so the
|
|
265
|
+
// settings publish path can mirror the card without an extra walk.
|
|
266
|
+
let lastRowCache = []
|
|
267
|
+
|
|
268
|
+
/** Masked connection rows for the card (never leaks passwords over the wire). */
|
|
269
|
+
export function summarizeRows(rows) {
|
|
270
|
+
return rows.map((r) => {
|
|
271
|
+
const srcId = slugify(r.title) + '_' + shortHash(r.wsPath) + (r.env === 'default' ? '' : '_' + envSlug(r.env))
|
|
272
|
+
return {
|
|
273
|
+
title: r.title,
|
|
274
|
+
path: r.wsPath,
|
|
275
|
+
env: r.env,
|
|
276
|
+
dsn: maskDsn(r.dsn),
|
|
277
|
+
source: r.source,
|
|
278
|
+
persisted: !!r.persisted,
|
|
279
|
+
srcId,
|
|
280
|
+
}
|
|
281
|
+
})
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** The last collected summary set (empty until the first walk). */
|
|
285
|
+
export function latestSummaries() {
|
|
286
|
+
return summarizeRows(lastRowCache)
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** mtime of the durable config inputs (workspace registry + credentials). */
|
|
290
|
+
export function configInputsMtime() {
|
|
333
291
|
let max = 0
|
|
334
292
|
for (const p of [join(dshHomeDir(), 'storages', 'workspace.json'), STORE_PATH]) {
|
|
335
293
|
try {
|
|
@@ -342,6 +300,46 @@ function configInputsMtime() {
|
|
|
342
300
|
return max
|
|
343
301
|
}
|
|
344
302
|
|
|
303
|
+
/**
|
|
304
|
+
* Walk every workspace × environment and produce the live connection rows
|
|
305
|
+
* (for the card's masked summaries) and the dbhub source list (for the toml
|
|
306
|
+
* and per-workspace tool names). Shared by `ensureRunning` and the settings
|
|
307
|
+
* publish path so the card always mirrors what the server would load.
|
|
308
|
+
* Environment naming: the 'default' env keeps the legacy bare source id
|
|
309
|
+
* (`<title>_<hash>`); extra environments append `_<envSlug>`.
|
|
310
|
+
* @returns `{ rows, sources }` — rows: [{ wsPath, title, env, dsn, source,
|
|
311
|
+
* persisted }]; sources: [{ id, dsn }].
|
|
312
|
+
*/
|
|
313
|
+
export async function collectSources(ctx, subprocess) {
|
|
314
|
+
const rows = []
|
|
315
|
+
const sources = []
|
|
316
|
+
try {
|
|
317
|
+
const workspaces = await listWorkspaces(ctx)
|
|
318
|
+
const persisted = listWorkspaceEnvironments(store)
|
|
319
|
+
const byPath = new Map()
|
|
320
|
+
for (const p of persisted) {
|
|
321
|
+
if (!byPath.has(p.wsPath)) byPath.set(p.wsPath, [])
|
|
322
|
+
byPath.get(p.wsPath).push(p)
|
|
323
|
+
}
|
|
324
|
+
for (const ws of workspaces) {
|
|
325
|
+
const title = ws.title || basename(ws.path)
|
|
326
|
+
const baseId = slugify(title) + '_' + shortHash(ws.path)
|
|
327
|
+
const resolved = await resolveWorkspaceEnvs(subprocess, ctx.get('fs'), ws.path, undefined)
|
|
328
|
+
const persistedEnvs = new Set((byPath.get(ws.path) || []).map((p) => p.env))
|
|
329
|
+
for (const r of resolved) {
|
|
330
|
+
const slug = envSlug(r.env)
|
|
331
|
+
const id = baseId + (slug ? '_' + slug : '')
|
|
332
|
+
rows.push({ wsPath: ws.path, title, env: r.env, dsn: r.dsn, source: r.source, persisted: persistedEnvs.has(r.env) })
|
|
333
|
+
sources.push({ id, dsn: r.dsn })
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
} catch (e) {
|
|
337
|
+
console.error('[dsh-dbhub-live] config generation failed: ' + String((e && e.message) || e))
|
|
338
|
+
}
|
|
339
|
+
lastRowCache = rows
|
|
340
|
+
return { rows, sources }
|
|
341
|
+
}
|
|
342
|
+
|
|
345
343
|
/**
|
|
346
344
|
* Make sure the persistent server is up and synced, initializing lazily on
|
|
347
345
|
* first use. Concurrent callers share one in-flight start. Regenerates the
|
|
@@ -357,18 +355,7 @@ export async function ensureRunning(ctx, subprocess) {
|
|
|
357
355
|
resetIdle(ctx)
|
|
358
356
|
return
|
|
359
357
|
}
|
|
360
|
-
|
|
361
|
-
try {
|
|
362
|
-
const workspaces = await listWorkspaces(ctx)
|
|
363
|
-
for (const ws of workspaces) {
|
|
364
|
-
const resolved = await resolveWorkspaceDsn(subprocess, ctx.get('fs'), ws.path, undefined)
|
|
365
|
-
if (resolved && resolved.dsn) {
|
|
366
|
-
sources.push({ id: slugify(ws.title) + '_' + shortHash(ws.path), dsn: resolved.dsn })
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
} catch (e) {
|
|
370
|
-
console.error('[dsh-dbhub-live] config generation failed: ' + String((e && e.message) || e))
|
|
371
|
-
}
|
|
358
|
+
const { sources } = await collectSources(ctx, subprocess)
|
|
372
359
|
lastInputMtime = inputMtime
|
|
373
360
|
const fp = fingerprintOf(sources)
|
|
374
361
|
// Never spawn with zero sources: dbhub treats an empty [[sources]] toml as
|
|
@@ -410,7 +397,6 @@ export async function ensureRunning(ctx, subprocess) {
|
|
|
410
397
|
state.setPhase('initializing')
|
|
411
398
|
const r = await spawnServer(subprocess, undefined)
|
|
412
399
|
if (r.error) throw new Error(r.error)
|
|
413
|
-
await syncToolsNow(ctx, subprocess, sources)
|
|
414
400
|
state.clearError()
|
|
415
401
|
state.setPhase('running')
|
|
416
402
|
refreshToolCount()
|
|
@@ -438,9 +424,9 @@ export async function runOnServer(ctx, subprocess, rawName, args, exec, label) {
|
|
|
438
424
|
try {
|
|
439
425
|
await ensureRunning(ctx, subprocess)
|
|
440
426
|
} catch (e) {
|
|
441
|
-
return { ok: false, text: '
|
|
427
|
+
return { ok: false, text: currentT('result.serverStartFail', { msg: String((e && e.message) || e) }) }
|
|
442
428
|
}
|
|
443
|
-
if (!server.client) return { ok: false, text: '
|
|
429
|
+
if (!server.client) return { ok: false, text: currentT('result.serverUnavailable') }
|
|
444
430
|
try {
|
|
445
431
|
const res = await server.client.request('tools/call', { name: rawName, arguments: args || {} })
|
|
446
432
|
const textOf = (r) => {
|
|
@@ -453,7 +439,7 @@ export async function runOnServer(ctx, subprocess, rawName, args, exec, label) {
|
|
|
453
439
|
}
|
|
454
440
|
resetIdle(ctx)
|
|
455
441
|
const prefix = label ? label + '\n' : ''
|
|
456
|
-
if (res && res.isError) return { ok: false, text:
|
|
442
|
+
if (res && res.isError) return { ok: false, text: textOf(res) }
|
|
457
443
|
if (res && res.structuredContent !== undefined) {
|
|
458
444
|
return { ok: true, text: prefix + JSON.stringify(res.structuredContent, null, 2) }
|
|
459
445
|
}
|
|
@@ -463,9 +449,9 @@ export async function runOnServer(ctx, subprocess, rawName, args, exec, label) {
|
|
|
463
449
|
}
|
|
464
450
|
}
|
|
465
451
|
|
|
466
|
-
// Shared "plugin disabled" copy for every dbhub tool.
|
|
452
|
+
// Shared "plugin disabled" copy for every dbhub tool (locale-aware).
|
|
467
453
|
export function disabledMessage() {
|
|
468
|
-
return '
|
|
454
|
+
return currentT('result.disabled')
|
|
469
455
|
}
|
|
470
456
|
|
|
471
457
|
/** Whether the persistent server currently answers calls. */
|
package/lib/options.mjs
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// dsh-dbhub-live: deployment-tunable options.
|
|
2
|
+
//
|
|
3
|
+
// Editable through the Settings -> Plugins -> dsh-dbhub-live card. Precedence:
|
|
4
|
+
// user settings (settings document) > process environment > built-in default
|
|
5
|
+
// Environment variables only seed the boot values; once the settings document
|
|
6
|
+
// claims a field (first publish writes the section), that field is owned by
|
|
7
|
+
// the settings UI until it is unset — so an env change no longer applies to a
|
|
8
|
+
// claimed field. This mirrors how the dsh settings system treats user layers.
|
|
9
|
+
|
|
10
|
+
const BUILTIN_DEFAULTS = {
|
|
11
|
+
dbhubPackage: '@bytebase/dbhub',
|
|
12
|
+
updateIntervalDays: 7,
|
|
13
|
+
idleMinutes: 10,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function envNumber(name, fallback) {
|
|
17
|
+
const raw = process.env[name]
|
|
18
|
+
if (raw === undefined || raw === '') return fallback
|
|
19
|
+
const n = Number(raw)
|
|
20
|
+
return Number.isFinite(n) && n >= 0 ? n : fallback
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const options = {
|
|
24
|
+
dbhubPackage: process.env.DSH_DBHUB_PACKAGE || BUILTIN_DEFAULTS.dbhubPackage,
|
|
25
|
+
updateIntervalDays: envNumber('DSH_DBHUB_UPDATE_DAYS', BUILTIN_DEFAULTS.updateIntervalDays),
|
|
26
|
+
idleMinutes: BUILTIN_DEFAULTS.idleMinutes,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const listeners = new Set()
|
|
30
|
+
|
|
31
|
+
function emit() {
|
|
32
|
+
for (const fn of [...listeners]) {
|
|
33
|
+
try {
|
|
34
|
+
fn()
|
|
35
|
+
} catch (e) {
|
|
36
|
+
/* contain listener failures */
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Subscribe to option changes; returns the disposer. */
|
|
42
|
+
export function subscribe(fn) {
|
|
43
|
+
listeners.add(fn)
|
|
44
|
+
return () => listeners.delete(fn)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Detached JSON snapshot of the settings-exposed options. `dbhubPackage` is
|
|
49
|
+
* deliberately excluded: the auto-install package stays an environment-seeded
|
|
50
|
+
* internal knob (DSH_DBHUB_PACKAGE / built-in default), not a settings field.
|
|
51
|
+
*/
|
|
52
|
+
export function snapshot() {
|
|
53
|
+
return {
|
|
54
|
+
updateIntervalDays: options.updateIntervalDays,
|
|
55
|
+
idleMinutes: options.idleMinutes,
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Read one option by id ('dbhubPackage' is internal-only).
|
|
61
|
+
* @param id - 'dbhubPackage' | 'updateIntervalDays' | 'idleMinutes'.
|
|
62
|
+
* @returns the current value.
|
|
63
|
+
*/
|
|
64
|
+
export function get(id) {
|
|
65
|
+
return options[id]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Per-field validators for the settings-exposed options; 'dbhubPackage' is
|
|
69
|
+
// deliberately not patchable (it stays an env/internal knob).
|
|
70
|
+
const VALIDATORS = {
|
|
71
|
+
updateIntervalDays: (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0,
|
|
72
|
+
idleMinutes: (v) => typeof v === 'number' && Number.isFinite(v) && v > 0,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Apply a validated patch onto the options.
|
|
77
|
+
* @param patch - partial option values (prefixes the env seed at boot).
|
|
78
|
+
* @returns whether any option actually changed.
|
|
79
|
+
*/
|
|
80
|
+
export function applyPatch(patch) {
|
|
81
|
+
if (!patch || typeof patch !== 'object' || Array.isArray(patch)) return false
|
|
82
|
+
let changed = false
|
|
83
|
+
for (const [key, validator] of Object.entries(VALIDATORS)) {
|
|
84
|
+
const value = patch[key]
|
|
85
|
+
if (value === undefined) continue
|
|
86
|
+
if (!validator(value)) continue
|
|
87
|
+
const next = key === 'dbhubPackage' ? String(value).trim() : value
|
|
88
|
+
if (next === options[key]) continue
|
|
89
|
+
options[key] = next
|
|
90
|
+
changed = true
|
|
91
|
+
}
|
|
92
|
+
if (changed) emit()
|
|
93
|
+
return changed
|
|
94
|
+
}
|
package/lib/runtime.mjs
CHANGED
|
@@ -11,17 +11,12 @@
|
|
|
11
11
|
import { existsSync, statSync, readdirSync, mkdirSync } from 'node:fs'
|
|
12
12
|
import { join } from 'node:path'
|
|
13
13
|
import { DATA_DIR, RUNTIME_DIR, runtime, saveRuntime, ensureStorageDir } from './config.mjs'
|
|
14
|
+
import * as options from './options.mjs'
|
|
14
15
|
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
// himself (PATH / mise) is never
|
|
18
|
-
//
|
|
19
|
-
const DBHUB_PACKAGE = process.env.DSH_DBHUB_PACKAGE || '@bytebase/dbhub'
|
|
20
|
-
const DBHUB_UPDATE_MS = (() => {
|
|
21
|
-
const raw = process.env.DSH_DBHUB_UPDATE_DAYS
|
|
22
|
-
const n = raw !== undefined && raw !== '' && Number(raw) >= 0 ? Number(raw) : 7
|
|
23
|
-
return n * 24 * 60 * 60 * 1000
|
|
24
|
-
})()
|
|
16
|
+
// Auto-update interval and install package are deployment-tunable options
|
|
17
|
+
// (Settings -> Plugins -> dsh-dbhub-live); only our own managed install is
|
|
18
|
+
// ever refreshed — a dbhub the user installed himself (PATH / mise) is never
|
|
19
|
+
// touched.
|
|
25
20
|
|
|
26
21
|
// ── executable lookup ─────────────────────────────────────────────────────
|
|
27
22
|
|
|
@@ -97,8 +92,8 @@ let installingDbhub // single in-flight install guard
|
|
|
97
92
|
|
|
98
93
|
// Install the dbhub MCP server once, into a private prefix under the storage
|
|
99
94
|
// dir. Uses `npm install --prefix <RUNTIME_DIR>` (deterministic, offline-safe
|
|
100
|
-
// afterwards) rather than `npx <pkg>` per call.
|
|
101
|
-
//
|
|
95
|
+
// afterwards) rather than `npx <pkg>` per call. The package/version is a
|
|
96
|
+
// configurable option (default installs the latest).
|
|
102
97
|
async function installDbhub(subprocess, signal) {
|
|
103
98
|
if (installingDbhub) return installingDbhub
|
|
104
99
|
if (!subprocess) {
|
|
@@ -115,9 +110,10 @@ async function installDbhub(subprocess, signal) {
|
|
|
115
110
|
if (!ensureStorageDir()) {
|
|
116
111
|
throw new Error('无法创建 dbhub 运行时目录 ' + DATA_DIR + '(写入被拦截)')
|
|
117
112
|
}
|
|
113
|
+
const dbhubPackage = options.get('dbhubPackage') || '@bytebase/dbhub'
|
|
118
114
|
const args = [
|
|
119
115
|
'install',
|
|
120
|
-
|
|
116
|
+
dbhubPackage,
|
|
121
117
|
'--prefix', RUNTIME_DIR,
|
|
122
118
|
'--no-save', '--no-fund', '--no-audit',
|
|
123
119
|
'--loglevel=error',
|
|
@@ -159,7 +155,7 @@ async function installDbhub(subprocess, signal) {
|
|
|
159
155
|
throw new Error('npm 安装完成,但未在 ' + RUNTIME_DIR + ' 下找到 dbhub 可执行文件')
|
|
160
156
|
}
|
|
161
157
|
runtime.dbhubInstallAt = Date.now()
|
|
162
|
-
console.log('[dsh-dbhub-live] 已自动安装 dbhub (' +
|
|
158
|
+
console.log('[dsh-dbhub-live] 已自动安装 dbhub (' + dbhubPackage + ') → ' + exe)
|
|
163
159
|
return exe
|
|
164
160
|
})()
|
|
165
161
|
try {
|
|
@@ -209,8 +205,10 @@ export function isAutoManagedExe() {
|
|
|
209
205
|
export async function maybeRefreshDbhub(subprocess) {
|
|
210
206
|
if (!isAutoManagedExe()) return
|
|
211
207
|
if (!(runtime.dbhubInstallAt > 0)) return
|
|
212
|
-
|
|
213
|
-
if (
|
|
208
|
+
const intervalDays = options.get('updateIntervalDays')
|
|
209
|
+
if (!(intervalDays > 0)) return // auto-update disabled
|
|
210
|
+
const intervalMs = intervalDays * 24 * 60 * 60 * 1000
|
|
211
|
+
if (Date.now() - runtime.dbhubInstallAt < intervalMs) return
|
|
214
212
|
try {
|
|
215
213
|
await installDbhub(subprocess, undefined)
|
|
216
214
|
console.log('[dsh-dbhub-live] dbhub 已自动更新到最新版')
|