dsh-db-studio 0.0.0-stage → 1.0.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/CHANGELOG.md +20 -0
- package/LICENSE +21 -0
- package/README.en.md +94 -0
- package/README.md +91 -2
- package/cordis.patch.yml +11 -0
- package/docs/conversation.png +0 -0
- package/docs/demo.gif +0 -0
- package/docs/panel.png +0 -0
- package/lib/client.js +2313 -0
- package/lib/drivers.js +456 -0
- package/lib/home.js +38 -0
- package/lib/index.js +481 -0
- package/lib/service.js +448 -0
- package/lib/session-store.js +223 -0
- package/lib/store.js +203 -0
- package/package.json +69 -4
package/lib/store.js
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connection store for dsh-db-studio.
|
|
3
|
+
*
|
|
4
|
+
* Connections live in `<home>/dsh-db-studio/connections.json` (mode 0600,
|
|
5
|
+
* directory 0700). The browser never receives a password: `publicConnection()`
|
|
6
|
+
* strips it and reports `hasPassword` instead. Passwords are stored in the
|
|
7
|
+
* local file because a database client has to be able to reconnect without
|
|
8
|
+
* the user retyping them; the file permissions are the boundary, and the
|
|
9
|
+
* README says so plainly.
|
|
10
|
+
*
|
|
11
|
+
* @module dsh-db-studio/store
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { randomUUID } from 'node:crypto'
|
|
15
|
+
import { chmod, mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
|
|
16
|
+
import { join } from 'node:path'
|
|
17
|
+
import { absolutePath, studioHome } from './home.js'
|
|
18
|
+
|
|
19
|
+
const FILE_MODE = 0o600
|
|
20
|
+
const DIR_MODE = 0o700
|
|
21
|
+
|
|
22
|
+
/** Connection kinds this plugin knows how to open. */
|
|
23
|
+
export const CONNECTION_KINDS = ['sqlite', 'postgres', 'mysql']
|
|
24
|
+
|
|
25
|
+
/** Default TCP ports per kind. */
|
|
26
|
+
const DEFAULT_PORTS = { postgres: 5432, mysql: 3306 }
|
|
27
|
+
|
|
28
|
+
/** Trimmed string or undefined. */
|
|
29
|
+
function text(value) {
|
|
30
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** A boolean with a default. */
|
|
34
|
+
function bool(value, fallback) {
|
|
35
|
+
return typeof value === 'boolean' ? value : fallback
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A port number, or the kind's default. */
|
|
39
|
+
function portOf(value, kind) {
|
|
40
|
+
const n = typeof value === 'number' ? value : Number.parseInt(String(value ?? ''), 10)
|
|
41
|
+
return Number.isInteger(n) && n > 0 && n < 65536 ? n : DEFAULT_PORTS[kind]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Validate and normalize one connection.
|
|
46
|
+
*
|
|
47
|
+
* Throws a plain Error with a user-facing Chinese/English message when the
|
|
48
|
+
* shape cannot be stored. `existing` supplies the fields an update may omit
|
|
49
|
+
* (notably the password).
|
|
50
|
+
*/
|
|
51
|
+
export function normalizeConnection(input, existing = null) {
|
|
52
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
53
|
+
throw new Error('连接配置格式不正确 / invalid connection object')
|
|
54
|
+
}
|
|
55
|
+
const kind = text(input.kind) ?? existing?.kind
|
|
56
|
+
if (!CONNECTION_KINDS.includes(kind)) {
|
|
57
|
+
throw new Error('连接类型必须是 sqlite、postgres 或 mysql / kind must be sqlite, postgres or mysql')
|
|
58
|
+
}
|
|
59
|
+
const now = new Date().toISOString()
|
|
60
|
+
const out = {
|
|
61
|
+
id: existing?.id ?? text(input.id) ?? randomUUID(),
|
|
62
|
+
name: text(input.name) ?? existing?.name ?? (kind === 'sqlite' ? 'SQLite 数据库' : kind === 'postgres' ? 'PostgreSQL' : 'MySQL'),
|
|
63
|
+
kind,
|
|
64
|
+
readOnly: bool(input.readOnly, existing?.readOnly ?? false),
|
|
65
|
+
createdAt: existing?.createdAt ?? now,
|
|
66
|
+
updatedAt: now,
|
|
67
|
+
}
|
|
68
|
+
if (typeof input.demo === 'boolean') out.demo = input.demo
|
|
69
|
+
else if (existing?.demo !== undefined) out.demo = existing.demo
|
|
70
|
+
|
|
71
|
+
if (kind === 'sqlite') {
|
|
72
|
+
const file = text(input.file) ?? existing?.file
|
|
73
|
+
if (!file) throw new Error('SQLite 连接需要文件路径 / a SQLite connection needs a file path')
|
|
74
|
+
out.file = absolutePath(file)
|
|
75
|
+
// SQLite credentials do not exist; keep the rest of the object clean.
|
|
76
|
+
return out
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
out.host = text(input.host) ?? existing?.host ?? '127.0.0.1'
|
|
80
|
+
out.port = portOf(input.port ?? existing?.port, kind)
|
|
81
|
+
out.database = text(input.database) ?? existing?.database
|
|
82
|
+
if (!out.database) throw new Error('请填写数据库名 / a database name is required')
|
|
83
|
+
out.user = text(input.user) ?? existing?.user ?? (kind === 'postgres' ? 'postgres' : 'root')
|
|
84
|
+
if (typeof input.ssl === 'boolean') out.ssl = input.ssl
|
|
85
|
+
else if (existing?.ssl !== undefined) out.ssl = existing.ssl
|
|
86
|
+
else out.ssl = false
|
|
87
|
+
|
|
88
|
+
if (typeof input.password === 'string' && input.password !== '') out.password = input.password
|
|
89
|
+
else if (existing?.password !== undefined) out.password = existing.password
|
|
90
|
+
if (input.clearPassword === true) delete out.password
|
|
91
|
+
return out
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The browser-safe projection of a connection: no password, ever. */
|
|
95
|
+
export function publicConnection(connection) {
|
|
96
|
+
if (!connection) return null
|
|
97
|
+
const { password, ...rest } = connection
|
|
98
|
+
return { ...rest, hasPassword: typeof password === 'string' && password.length > 0 }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** A human-readable one-line target for a connection. */
|
|
102
|
+
export function connectionTarget(connection) {
|
|
103
|
+
if (connection.kind === 'sqlite') return connection.file
|
|
104
|
+
return `${connection.user}@${connection.host}:${connection.port}/${connection.database}`
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The connection file is shared by every reload of the plugin in this home. */
|
|
108
|
+
export class ConnectionStore {
|
|
109
|
+
constructor(home) {
|
|
110
|
+
this.home = home
|
|
111
|
+
this.dir = studioHome(home)
|
|
112
|
+
this.file = join(this.dir, 'connections.json')
|
|
113
|
+
this.connections = []
|
|
114
|
+
this.loaded = false
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Read from disk. A missing or malformed file becomes an empty store. */
|
|
118
|
+
async load() {
|
|
119
|
+
if (this.loaded) return this
|
|
120
|
+
this.loaded = true
|
|
121
|
+
try {
|
|
122
|
+
const raw = await readFile(this.file, 'utf8')
|
|
123
|
+
const parsed = JSON.parse(raw)
|
|
124
|
+
if (Array.isArray(parsed?.connections)) {
|
|
125
|
+
this.connections = parsed.connections.filter((row) => row && typeof row === 'object' && row.id)
|
|
126
|
+
}
|
|
127
|
+
} catch (error) {
|
|
128
|
+
if (error?.code !== 'ENOENT') {
|
|
129
|
+
console.warn('dsh-db-studio: ignoring unreadable connections file —', error?.message ?? error)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return this
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Persist atomically: write a sibling then rename into place. */
|
|
136
|
+
async save() {
|
|
137
|
+
await mkdir(this.dir, { recursive: true, mode: DIR_MODE })
|
|
138
|
+
const body = JSON.stringify({ version: 1, connections: this.connections }, null, 2) + '\n'
|
|
139
|
+
const temp = `${this.file}.${process.pid}.${Date.now()}.tmp`
|
|
140
|
+
await writeFile(temp, body, { mode: FILE_MODE })
|
|
141
|
+
await rename(temp, this.file)
|
|
142
|
+
// rename preserves the temp file's mode; chmod is belt-and-braces for a
|
|
143
|
+
// pre-existing file whose mode was widened outside this plugin.
|
|
144
|
+
await chmod(this.file, FILE_MODE).catch(() => {})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** All connections, secrets redacted, in insertion order. */
|
|
148
|
+
list() {
|
|
149
|
+
return this.connections.map(publicConnection)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Raw connection by id (host-side use only). */
|
|
153
|
+
get(id) {
|
|
154
|
+
return this.connections.find((row) => row.id === id) ?? null
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** The first connection, used when an agent tool omits an id. */
|
|
158
|
+
first() {
|
|
159
|
+
return this.connections[0] ?? null
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Add a validated connection and persist it. */
|
|
163
|
+
async add(input) {
|
|
164
|
+
const connection = normalizeConnection(input)
|
|
165
|
+
if (this.connections.some((row) => row.id === connection.id)) {
|
|
166
|
+
connection.id = randomUUID()
|
|
167
|
+
}
|
|
168
|
+
this.connections.push(connection)
|
|
169
|
+
await this.save()
|
|
170
|
+
return publicConnection(connection)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Patch an existing connection. `undefined` keeps a field; password is special. */
|
|
174
|
+
async update(id, patch) {
|
|
175
|
+
const index = this.connections.findIndex((row) => row.id === id)
|
|
176
|
+
if (index === -1) throw new Error('连接不存在 / connection not found')
|
|
177
|
+
const merged = normalizeConnection({ ...this.connections[index], ...patch }, this.connections[index])
|
|
178
|
+
merged.id = id
|
|
179
|
+
this.connections[index] = merged
|
|
180
|
+
await this.save()
|
|
181
|
+
return publicConnection(merged)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Remove a connection. The demo connection is recreated by `ensureDemo`. */
|
|
185
|
+
async remove(id) {
|
|
186
|
+
const before = this.connections.length
|
|
187
|
+
this.connections = this.connections.filter((row) => row.id !== id)
|
|
188
|
+
if (this.connections.length === before) throw new Error('连接不存在 / connection not found')
|
|
189
|
+
await this.save()
|
|
190
|
+
return true
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Replace the whole list (used by demo seeding); persists immediately. */
|
|
194
|
+
async replaceAll(connections) {
|
|
195
|
+
this.connections = connections
|
|
196
|
+
await this.save()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Remove the store (tests). */
|
|
200
|
+
async destroy() {
|
|
201
|
+
await rm(this.dir, { recursive: true, force: true })
|
|
202
|
+
}
|
|
203
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,71 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-db-studio",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "Connect to databases in DeepSeek Harness (DSH), then query and edit data right in the conversation.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"dsh",
|
|
9
|
+
"deepseek-harness",
|
|
10
|
+
"dsh-plugin",
|
|
11
|
+
"database",
|
|
12
|
+
"sqlite",
|
|
13
|
+
"postgresql",
|
|
14
|
+
"mysql",
|
|
15
|
+
"conversation",
|
|
16
|
+
"ai-agent"
|
|
17
|
+
],
|
|
18
|
+
"main": "./lib/index.js",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": "./lib/index.js",
|
|
21
|
+
"./client": "./lib/client.js",
|
|
22
|
+
"./package.json": "./package.json"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"lib",
|
|
26
|
+
"docs",
|
|
27
|
+
"cordis.patch.yml",
|
|
28
|
+
"README.md",
|
|
29
|
+
"README.en.md",
|
|
30
|
+
"CHANGELOG.md"
|
|
31
|
+
],
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=22.13",
|
|
34
|
+
"dsh": ">=0.2.1-rc.3"
|
|
35
|
+
},
|
|
36
|
+
"dsh": {
|
|
37
|
+
"bundle": {
|
|
38
|
+
"patch": "./cordis.patch.yml"
|
|
39
|
+
},
|
|
40
|
+
"client": {
|
|
41
|
+
"platform": "web",
|
|
42
|
+
"inject": [
|
|
43
|
+
"@deepseek-ai/dsh-client-locale",
|
|
44
|
+
"@deepseek-ai/dsh-client-ui-layout",
|
|
45
|
+
"@deepseek-ai/dsh-client-ui-sidebar",
|
|
46
|
+
"@deepseek-ai/dsh-client-ui-commands"
|
|
47
|
+
]
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"mysql2": "^3.24.6",
|
|
52
|
+
"pg": "^8.23.1"
|
|
53
|
+
},
|
|
54
|
+
"scripts": {
|
|
55
|
+
"test": "node --test test/*.test.js",
|
|
56
|
+
"check": "node --check lib/index.js && node --check lib/service.js && node --check lib/drivers.js && node --check lib/store.js && node --check lib/home.js && node --check lib/client.js"
|
|
57
|
+
},
|
|
58
|
+
"repository": {
|
|
59
|
+
"type": "git",
|
|
60
|
+
"url": "git+https://github.com/etShaw-zh/dsh-db-studio.git"
|
|
61
|
+
},
|
|
62
|
+
"author": "Jianjun Xiao <et_shaw@126.com>",
|
|
63
|
+
"homepage": "https://github.com/etShaw-zh/dsh-db-studio#readme",
|
|
64
|
+
"bugs": {
|
|
65
|
+
"url": "https://github.com/etShaw-zh/dsh-db-studio/issues"
|
|
66
|
+
},
|
|
67
|
+
"publishConfig": {
|
|
68
|
+
"access": "public",
|
|
69
|
+
"registry": "https://registry.npmjs.org/"
|
|
70
|
+
}
|
|
71
|
+
}
|