@zhin.js/scaffold-wizard 1.1.0 → 1.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/README.md +5 -0
- package/lib/adapter.js +1 -1
- package/lib/adapter.js.map +1 -1
- package/lib/ai.d.ts +1 -7
- package/lib/ai.d.ts.map +1 -1
- package/lib/ai.js +0 -63
- package/lib/ai.js.map +1 -1
- package/lib/apply.d.ts +5 -5
- package/lib/apply.d.ts.map +1 -1
- package/lib/apply.js +13 -23
- package/lib/apply.js.map +1 -1
- package/lib/database-definitions.d.ts +28 -0
- package/lib/database-definitions.d.ts.map +1 -0
- package/lib/database-definitions.js +290 -0
- package/lib/database-definitions.js.map +1 -0
- package/lib/database.d.ts.map +1 -1
- package/lib/database.js +69 -51
- package/lib/database.js.map +1 -1
- package/lib/env.d.ts +6 -0
- package/lib/env.d.ts.map +1 -1
- package/lib/env.js +51 -0
- package/lib/env.js.map +1 -1
- package/lib/index.d.ts +8 -6
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +7 -6
- package/lib/index.js.map +1 -1
- package/lib/optional-peers.d.ts.map +1 -1
- package/lib/optional-peers.js +5 -16
- package/lib/optional-peers.js.map +1 -1
- package/lib/project-config-plan.d.ts +2 -1
- package/lib/project-config-plan.d.ts.map +1 -1
- package/lib/project-config-plan.js +29 -83
- package/lib/project-config-plan.js.map +1 -1
- package/lib/project-deps.d.ts +1 -1
- package/lib/project-deps.d.ts.map +1 -1
- package/lib/project-deps.js +5 -7
- package/lib/project-deps.js.map +1 -1
- package/lib/types.d.ts +4 -3
- package/lib/types.d.ts.map +1 -1
- package/lib/types.js +6 -8
- package/lib/types.js.map +1 -1
- package/lib/zhin-stack-deps.d.ts +8 -6
- package/lib/zhin-stack-deps.d.ts.map +1 -1
- package/lib/zhin-stack-deps.js +16 -25
- package/lib/zhin-stack-deps.js.map +1 -1
- package/package.json +3 -3
- package/src/adapter.ts +1 -1
- package/src/ai.ts +1 -70
- package/src/apply.ts +17 -23
- package/src/database-definitions.ts +336 -0
- package/src/database.ts +77 -52
- package/src/env.ts +52 -0
- package/src/index.ts +18 -6
- package/src/optional-peers.ts +4 -14
- package/src/project-config-plan.ts +35 -79
- package/src/project-deps.ts +5 -6
- package/src/types.ts +13 -10
- package/src/zhin-stack-deps.ts +16 -27
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
import type { DatabaseConfig } from './types.js';
|
|
2
|
+
|
|
3
|
+
export type DatabaseDialect = DatabaseConfig['dialect'];
|
|
4
|
+
|
|
5
|
+
export interface DatabaseDriverDefinition {
|
|
6
|
+
readonly package: string;
|
|
7
|
+
readonly version: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface DatabaseDialectDefinition {
|
|
11
|
+
readonly dialect: DatabaseDialect;
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly description: string;
|
|
14
|
+
/** Concrete service version covered by the repository's live acceptance tests. */
|
|
15
|
+
readonly verifiedServer: string;
|
|
16
|
+
readonly driver?: DatabaseDriverDefinition;
|
|
17
|
+
readonly schema: Readonly<Record<string, unknown>>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type SchemaIssueKind = 'env_missing' | 'invalid';
|
|
21
|
+
|
|
22
|
+
export interface DatabaseSchemaIssue {
|
|
23
|
+
readonly kind: SchemaIssueKind;
|
|
24
|
+
readonly path: string;
|
|
25
|
+
readonly message: string;
|
|
26
|
+
readonly envKey?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const text = (defaultValue?: string, extras: Record<string, unknown> = {}) => ({
|
|
30
|
+
type: 'string',
|
|
31
|
+
minLength: 1,
|
|
32
|
+
...(defaultValue === undefined ? {} : { default: defaultValue }),
|
|
33
|
+
...extras,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const port = (defaultValue: number) => ({
|
|
37
|
+
type: 'integer',
|
|
38
|
+
minimum: 1,
|
|
39
|
+
maximum: 65535,
|
|
40
|
+
default: defaultValue,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export const DATABASE_DIALECT_DEFINITIONS: readonly DatabaseDialectDefinition[] = Object.freeze([
|
|
44
|
+
{
|
|
45
|
+
dialect: 'sqlite',
|
|
46
|
+
name: 'SQLite',
|
|
47
|
+
description: '推荐,零配置,适合单机 Bot',
|
|
48
|
+
verifiedServer: 'Node.js 内置 node:sqlite(Node.js 22.5+)',
|
|
49
|
+
schema: {
|
|
50
|
+
type: 'object',
|
|
51
|
+
additionalProperties: true,
|
|
52
|
+
required: ['dialect'],
|
|
53
|
+
properties: {
|
|
54
|
+
dialect: { const: 'sqlite' },
|
|
55
|
+
filename: text('./data/bot.db'),
|
|
56
|
+
mode: { type: 'string', enum: ['wal', 'delete', 'memory'], default: 'wal' },
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
dialect: 'mysql',
|
|
62
|
+
name: 'MySQL',
|
|
63
|
+
description: '关系型数据库',
|
|
64
|
+
verifiedServer: 'MySQL 8.4 LTS',
|
|
65
|
+
driver: { package: 'mysql2', version: '^3.23.2' },
|
|
66
|
+
schema: {
|
|
67
|
+
type: 'object',
|
|
68
|
+
additionalProperties: true,
|
|
69
|
+
required: ['dialect'],
|
|
70
|
+
properties: {
|
|
71
|
+
dialect: { const: 'mysql' },
|
|
72
|
+
host: text('127.0.0.1'),
|
|
73
|
+
port: port(3306),
|
|
74
|
+
user: text('root'),
|
|
75
|
+
password: { type: 'string' },
|
|
76
|
+
database: text('zhin_bot'),
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
dialect: 'pg',
|
|
82
|
+
name: 'PostgreSQL',
|
|
83
|
+
description: '关系型数据库,方言名为 pg',
|
|
84
|
+
verifiedServer: 'PostgreSQL 17',
|
|
85
|
+
driver: { package: 'pg', version: '^8.22.0' },
|
|
86
|
+
schema: {
|
|
87
|
+
type: 'object',
|
|
88
|
+
additionalProperties: true,
|
|
89
|
+
required: ['dialect'],
|
|
90
|
+
properties: {
|
|
91
|
+
dialect: { const: 'pg' },
|
|
92
|
+
host: text('127.0.0.1'),
|
|
93
|
+
port: port(5432),
|
|
94
|
+
user: text('postgres'),
|
|
95
|
+
password: { type: 'string' },
|
|
96
|
+
database: text('zhin_bot'),
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
dialect: 'mongodb',
|
|
102
|
+
name: 'MongoDB',
|
|
103
|
+
description: '文档数据库',
|
|
104
|
+
verifiedServer: 'MongoDB 8',
|
|
105
|
+
driver: { package: 'mongodb', version: '^7.5.0' },
|
|
106
|
+
schema: {
|
|
107
|
+
type: 'object',
|
|
108
|
+
additionalProperties: true,
|
|
109
|
+
required: ['dialect', 'url', 'dbName'],
|
|
110
|
+
properties: {
|
|
111
|
+
dialect: { const: 'mongodb' },
|
|
112
|
+
url: text('mongodb://127.0.0.1:27017', { pattern: '^mongodb(?:\\+srv)?://' }),
|
|
113
|
+
dbName: text('zhin_bot'),
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
dialect: 'redis',
|
|
119
|
+
name: 'Redis',
|
|
120
|
+
description: '键值数据库',
|
|
121
|
+
verifiedServer: 'Redis 8',
|
|
122
|
+
driver: { package: 'redis', version: '^6.1.0' },
|
|
123
|
+
schema: {
|
|
124
|
+
type: 'object',
|
|
125
|
+
additionalProperties: true,
|
|
126
|
+
required: ['dialect'],
|
|
127
|
+
properties: {
|
|
128
|
+
dialect: { const: 'redis' },
|
|
129
|
+
socket: {
|
|
130
|
+
type: 'object',
|
|
131
|
+
additionalProperties: true,
|
|
132
|
+
required: ['host', 'port'],
|
|
133
|
+
properties: {
|
|
134
|
+
host: text('127.0.0.1'),
|
|
135
|
+
port: port(6379),
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
password: { type: 'string' },
|
|
139
|
+
database: { type: 'integer', minimum: 0, default: 0 },
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
dialect: 'memory',
|
|
145
|
+
name: 'Memory',
|
|
146
|
+
description: '仅进程内,适合测试,不持久化',
|
|
147
|
+
verifiedServer: 'Zhin 内置实现',
|
|
148
|
+
schema: {
|
|
149
|
+
type: 'object',
|
|
150
|
+
additionalProperties: false,
|
|
151
|
+
required: ['dialect'],
|
|
152
|
+
properties: { dialect: { const: 'memory' } },
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
]);
|
|
156
|
+
|
|
157
|
+
const definitions = new Map(DATABASE_DIALECT_DEFINITIONS.map((definition) => [definition.dialect, definition]));
|
|
158
|
+
|
|
159
|
+
export function getDatabaseDialectDefinition(dialect: string): DatabaseDialectDefinition | undefined {
|
|
160
|
+
return definitions.get(dialect as DatabaseDialect);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function databaseChoiceLabel(definition: DatabaseDialectDefinition): string {
|
|
164
|
+
const driver = definition.driver
|
|
165
|
+
? `;驱动 ${definition.driver.package} ${definition.driver.version}`
|
|
166
|
+
: '';
|
|
167
|
+
return `${definition.name}(${definition.description};当前支持/实机验证 ${definition.verifiedServer}${driver})`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function expandEnvReferences(
|
|
171
|
+
input: string,
|
|
172
|
+
path: string,
|
|
173
|
+
env: Readonly<Record<string, string | undefined>>,
|
|
174
|
+
issues: DatabaseSchemaIssue[],
|
|
175
|
+
): string {
|
|
176
|
+
let output = '';
|
|
177
|
+
let index = 0;
|
|
178
|
+
while (index < input.length) {
|
|
179
|
+
const start = input.indexOf('${', index);
|
|
180
|
+
if (start < 0) break;
|
|
181
|
+
let cursor = start + 2;
|
|
182
|
+
const first = input[cursor];
|
|
183
|
+
if (first === undefined || !isEnvKeyStart(first)) {
|
|
184
|
+
output += input.slice(index, start + 1);
|
|
185
|
+
index = start + 1;
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
cursor += 1;
|
|
189
|
+
while (cursor < input.length && isEnvKeyCharacter(input[cursor]!)) cursor += 1;
|
|
190
|
+
const key = input.slice(start + 2, cursor);
|
|
191
|
+
let fallback: string | undefined;
|
|
192
|
+
if (input[cursor] === ':' && (input[cursor + 1] === '-' || input[cursor + 1] === '=')) {
|
|
193
|
+
const close = input.indexOf('}', cursor + 2);
|
|
194
|
+
if (close < 0) {
|
|
195
|
+
output += input.slice(index, start + 1);
|
|
196
|
+
index = start + 1;
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
fallback = input.slice(cursor + 2, close);
|
|
200
|
+
cursor = close;
|
|
201
|
+
}
|
|
202
|
+
if (input[cursor] !== '}') {
|
|
203
|
+
output += input.slice(index, start + 1);
|
|
204
|
+
index = start + 1;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
output += input.slice(index, start);
|
|
208
|
+
const resolved = env[key];
|
|
209
|
+
if ((resolved === undefined || resolved === '') && fallback !== undefined) {
|
|
210
|
+
output += fallback;
|
|
211
|
+
} else if (resolved === undefined) {
|
|
212
|
+
issues.push({
|
|
213
|
+
kind: 'env_missing',
|
|
214
|
+
path,
|
|
215
|
+
envKey: key,
|
|
216
|
+
message: `环境变量 ${key} 未设置(${path})`,
|
|
217
|
+
});
|
|
218
|
+
} else {
|
|
219
|
+
output += resolved;
|
|
220
|
+
}
|
|
221
|
+
index = cursor + 1;
|
|
222
|
+
}
|
|
223
|
+
return output + input.slice(index);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function isEnvKeyStart(character: string): boolean {
|
|
227
|
+
return (character >= 'A' && character <= 'Z')
|
|
228
|
+
|| (character >= 'a' && character <= 'z')
|
|
229
|
+
|| character === '_';
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function isEnvKeyCharacter(character: string): boolean {
|
|
233
|
+
return isEnvKeyStart(character) || (character >= '0' && character <= '9');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function validateValue(
|
|
237
|
+
schema: Record<string, unknown>,
|
|
238
|
+
rawValue: unknown,
|
|
239
|
+
path: string,
|
|
240
|
+
env: Readonly<Record<string, string | undefined>>,
|
|
241
|
+
issues: DatabaseSchemaIssue[],
|
|
242
|
+
): void {
|
|
243
|
+
let value = rawValue;
|
|
244
|
+
if (typeof value === 'string') {
|
|
245
|
+
const before = issues.length;
|
|
246
|
+
value = expandEnvReferences(value, path, env, issues);
|
|
247
|
+
if (issues.length > before) return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if ('const' in schema && value !== schema.const) {
|
|
251
|
+
issues.push({ kind: 'invalid', path, message: `${path} 必须是 ${JSON.stringify(schema.const)}` });
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const type = schema.type;
|
|
255
|
+
if (type === 'object') {
|
|
256
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
257
|
+
issues.push({ kind: 'invalid', path, message: `${path} 必须是对象` });
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const record = value as Record<string, unknown>;
|
|
261
|
+
const properties = (schema.properties ?? {}) as Record<string, Record<string, unknown>>;
|
|
262
|
+
if (schema.additionalProperties === false) {
|
|
263
|
+
for (const key of Object.keys(record)) {
|
|
264
|
+
if (!(key in properties)) {
|
|
265
|
+
issues.push({ kind: 'invalid', path: `${path}.${key}`, message: `${path}.${key} 不是受支持的配置项` });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
for (const required of (schema.required ?? []) as string[]) {
|
|
270
|
+
if (!(required in record) || record[required] === undefined || record[required] === null) {
|
|
271
|
+
issues.push({ kind: 'invalid', path: `${path}.${required}`, message: `${path}.${required} 不能为空` });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
for (const [key, propertySchema] of Object.entries(properties)) {
|
|
275
|
+
if (record[key] !== undefined) validateValue(propertySchema, record[key], `${path}.${key}`, env, issues);
|
|
276
|
+
}
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
if (type === 'string') {
|
|
280
|
+
if (typeof value !== 'string') {
|
|
281
|
+
issues.push({ kind: 'invalid', path, message: `${path} 必须是字符串` });
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
if (typeof schema.minLength === 'number' && value.length < schema.minLength) {
|
|
285
|
+
issues.push({ kind: 'invalid', path, message: `${path} 不能为空` });
|
|
286
|
+
}
|
|
287
|
+
if (typeof schema.pattern === 'string' && !new RegExp(schema.pattern, 'u').test(value)) {
|
|
288
|
+
issues.push({ kind: 'invalid', path, message: `${path} 格式无效` });
|
|
289
|
+
}
|
|
290
|
+
if (Array.isArray(schema.enum) && !schema.enum.includes(value)) {
|
|
291
|
+
issues.push({ kind: 'invalid', path, message: `${path} 必须是 ${schema.enum.join('、')} 之一` });
|
|
292
|
+
}
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if (type === 'integer') {
|
|
296
|
+
const numeric = typeof value === 'number'
|
|
297
|
+
? value
|
|
298
|
+
: typeof value === 'string' && /^\d+$/u.test(value.trim())
|
|
299
|
+
? Number(value.trim())
|
|
300
|
+
: Number.NaN;
|
|
301
|
+
if (!Number.isInteger(numeric)) {
|
|
302
|
+
issues.push({ kind: 'invalid', path, message: `${path} 必须是整数` });
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (typeof schema.minimum === 'number' && numeric < schema.minimum) {
|
|
306
|
+
issues.push({ kind: 'invalid', path, message: `${path} 不能小于 ${schema.minimum}` });
|
|
307
|
+
}
|
|
308
|
+
if (typeof schema.maximum === 'number' && numeric > schema.maximum) {
|
|
309
|
+
issues.push({ kind: 'invalid', path, message: `${path} 不能大于 ${schema.maximum}` });
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export function validateDatabaseConfig(
|
|
315
|
+
config: unknown,
|
|
316
|
+
env: Readonly<Record<string, string | undefined>> = {},
|
|
317
|
+
): readonly DatabaseSchemaIssue[] {
|
|
318
|
+
if (!config || typeof config !== 'object' || Array.isArray(config)) {
|
|
319
|
+
return [{ kind: 'invalid', path: 'database', message: 'database 必须是对象' }];
|
|
320
|
+
}
|
|
321
|
+
const dialect = (config as Record<string, unknown>).dialect;
|
|
322
|
+
if (typeof dialect !== 'string') {
|
|
323
|
+
return [{ kind: 'invalid', path: 'database.dialect', message: 'database.dialect 不能为空' }];
|
|
324
|
+
}
|
|
325
|
+
const definition = getDatabaseDialectDefinition(dialect);
|
|
326
|
+
if (!definition) {
|
|
327
|
+
return [{
|
|
328
|
+
kind: 'invalid',
|
|
329
|
+
path: 'database.dialect',
|
|
330
|
+
message: `不支持的数据库方言 ${JSON.stringify(dialect)}`,
|
|
331
|
+
}];
|
|
332
|
+
}
|
|
333
|
+
const issues: DatabaseSchemaIssue[] = [];
|
|
334
|
+
validateValue(definition.schema as Record<string, unknown>, config, 'database', env, issues);
|
|
335
|
+
return Object.freeze(issues);
|
|
336
|
+
}
|
package/src/database.ts
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
1
1
|
import inquirer from 'inquirer';
|
|
2
2
|
import { DatabaseConfig } from './types.js';
|
|
3
|
+
import {
|
|
4
|
+
DATABASE_DIALECT_DEFINITIONS,
|
|
5
|
+
databaseChoiceLabel,
|
|
6
|
+
getDatabaseDialectDefinition,
|
|
7
|
+
validateDatabaseConfig,
|
|
8
|
+
type DatabaseDialect,
|
|
9
|
+
} from './database-definitions.js';
|
|
10
|
+
|
|
11
|
+
function schemaDefault(dialect: DatabaseDialect, ...path: string[]): unknown {
|
|
12
|
+
let schema = getDatabaseDialectDefinition(dialect)?.schema as Record<string, unknown> | undefined;
|
|
13
|
+
for (const segment of path) {
|
|
14
|
+
const properties = schema?.properties as Record<string, Record<string, unknown>> | undefined;
|
|
15
|
+
schema = properties?.[segment];
|
|
16
|
+
}
|
|
17
|
+
return schema?.default;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function parseIntegerInput(input: unknown, minimum: number, maximum = Number.MAX_SAFE_INTEGER): number | string {
|
|
21
|
+
const raw = String(input).trim();
|
|
22
|
+
if (!/^\d+$/u.test(raw)) return `请输入有效的整数 (${minimum}-${maximum})`;
|
|
23
|
+
const value = Number(raw);
|
|
24
|
+
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
25
|
+
return `请输入有效的整数 (${minimum}-${maximum})`;
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
3
29
|
|
|
4
30
|
// 配置数据库选项
|
|
5
31
|
export async function configureDatabaseOptions(): Promise<DatabaseConfig> {
|
|
@@ -8,31 +34,42 @@ export async function configureDatabaseOptions(): Promise<DatabaseConfig> {
|
|
|
8
34
|
type: 'select',
|
|
9
35
|
name: 'dialect',
|
|
10
36
|
message: '选择数据库类型:',
|
|
11
|
-
choices:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
{ name: 'MongoDB', value: 'mongodb' },
|
|
16
|
-
{ name: 'Redis', value: 'redis' }
|
|
17
|
-
],
|
|
37
|
+
choices: DATABASE_DIALECT_DEFINITIONS.map((definition) => ({
|
|
38
|
+
name: databaseChoiceLabel(definition),
|
|
39
|
+
value: definition.dialect,
|
|
40
|
+
})),
|
|
18
41
|
default: 'sqlite'
|
|
19
42
|
}
|
|
20
43
|
]);
|
|
21
44
|
|
|
45
|
+
let config: DatabaseConfig;
|
|
22
46
|
switch (dialect) {
|
|
23
47
|
case 'sqlite':
|
|
24
|
-
|
|
48
|
+
config = await configureSQLite();
|
|
49
|
+
break;
|
|
25
50
|
case 'mysql':
|
|
26
|
-
|
|
51
|
+
config = await configureMySQL();
|
|
52
|
+
break;
|
|
27
53
|
case 'pg':
|
|
28
|
-
|
|
54
|
+
config = await configurePostgreSQL();
|
|
55
|
+
break;
|
|
29
56
|
case 'mongodb':
|
|
30
|
-
|
|
57
|
+
config = await configureMongoDB();
|
|
58
|
+
break;
|
|
31
59
|
case 'redis':
|
|
32
|
-
|
|
60
|
+
config = await configureRedis();
|
|
61
|
+
break;
|
|
62
|
+
case 'memory':
|
|
63
|
+
config = { dialect: 'memory' };
|
|
64
|
+
break;
|
|
33
65
|
default:
|
|
34
66
|
throw new Error(`未支持的数据库类型: ${dialect}`);
|
|
35
67
|
}
|
|
68
|
+
const issues = validateDatabaseConfig(config);
|
|
69
|
+
if (issues.length > 0) {
|
|
70
|
+
throw new TypeError(`数据库配置不符合 Schema:${issues.map((issue) => issue.message).join(';')}`);
|
|
71
|
+
}
|
|
72
|
+
return config;
|
|
36
73
|
}
|
|
37
74
|
|
|
38
75
|
// SQLite 配置
|
|
@@ -42,7 +79,7 @@ async function configureSQLite(): Promise<DatabaseConfig> {
|
|
|
42
79
|
type: 'input',
|
|
43
80
|
name: 'filename',
|
|
44
81
|
message: 'SQLite 数据库文件路径:',
|
|
45
|
-
default: '
|
|
82
|
+
default: schemaDefault('sqlite', 'filename'),
|
|
46
83
|
validate: (input: string) => {
|
|
47
84
|
if (!input.trim()) return '文件路径不能为空';
|
|
48
85
|
return true;
|
|
@@ -57,7 +94,7 @@ async function configureSQLite(): Promise<DatabaseConfig> {
|
|
|
57
94
|
{ name: 'DELETE (默认)', value: 'delete' },
|
|
58
95
|
{ name: 'MEMORY (内存数据库)', value: 'memory' }
|
|
59
96
|
],
|
|
60
|
-
default: '
|
|
97
|
+
default: schemaDefault('sqlite', 'mode')
|
|
61
98
|
}
|
|
62
99
|
]);
|
|
63
100
|
|
|
@@ -75,26 +112,23 @@ async function configureMySQL(): Promise<DatabaseConfig> {
|
|
|
75
112
|
type: 'input',
|
|
76
113
|
name: 'host',
|
|
77
114
|
message: 'MySQL 主机地址:',
|
|
78
|
-
default: '
|
|
115
|
+
default: schemaDefault('mysql', 'host')
|
|
79
116
|
},
|
|
80
117
|
{
|
|
81
118
|
type: 'input',
|
|
82
119
|
name: 'port',
|
|
83
120
|
message: 'MySQL 端口:',
|
|
84
|
-
default: '
|
|
121
|
+
default: String(schemaDefault('mysql', 'port')),
|
|
85
122
|
validate: (input: string) => {
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
return '请输入有效的端口号 (1-65535)';
|
|
89
|
-
}
|
|
90
|
-
return true;
|
|
123
|
+
const value = parseIntegerInput(input, 1, 65535);
|
|
124
|
+
return typeof value === 'number' ? true : value;
|
|
91
125
|
}
|
|
92
126
|
},
|
|
93
127
|
{
|
|
94
128
|
type: 'input',
|
|
95
129
|
name: 'user',
|
|
96
130
|
message: 'MySQL 用户名:',
|
|
97
|
-
default: '
|
|
131
|
+
default: schemaDefault('mysql', 'user')
|
|
98
132
|
},
|
|
99
133
|
{
|
|
100
134
|
type: 'password',
|
|
@@ -105,7 +139,7 @@ async function configureMySQL(): Promise<DatabaseConfig> {
|
|
|
105
139
|
type: 'input',
|
|
106
140
|
name: 'database',
|
|
107
141
|
message: 'MySQL 数据库名:',
|
|
108
|
-
default: '
|
|
142
|
+
default: schemaDefault('mysql', 'database'),
|
|
109
143
|
validate: (input: string) => {
|
|
110
144
|
if (!input.trim()) return '数据库名不能为空';
|
|
111
145
|
return true;
|
|
@@ -116,7 +150,7 @@ async function configureMySQL(): Promise<DatabaseConfig> {
|
|
|
116
150
|
return {
|
|
117
151
|
dialect: 'mysql',
|
|
118
152
|
host: config.host,
|
|
119
|
-
port:
|
|
153
|
+
port: Number(config.port),
|
|
120
154
|
user: config.user,
|
|
121
155
|
password: config.password,
|
|
122
156
|
database: config.database
|
|
@@ -130,26 +164,23 @@ async function configurePostgreSQL(): Promise<DatabaseConfig> {
|
|
|
130
164
|
type: 'input',
|
|
131
165
|
name: 'host',
|
|
132
166
|
message: 'PostgreSQL 主机地址:',
|
|
133
|
-
default: '
|
|
167
|
+
default: schemaDefault('pg', 'host')
|
|
134
168
|
},
|
|
135
169
|
{
|
|
136
170
|
type: 'input',
|
|
137
171
|
name: 'port',
|
|
138
172
|
message: 'PostgreSQL 端口:',
|
|
139
|
-
default: '
|
|
173
|
+
default: String(schemaDefault('pg', 'port')),
|
|
140
174
|
validate: (input: string) => {
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
return '请输入有效的端口号 (1-65535)';
|
|
144
|
-
}
|
|
145
|
-
return true;
|
|
175
|
+
const value = parseIntegerInput(input, 1, 65535);
|
|
176
|
+
return typeof value === 'number' ? true : value;
|
|
146
177
|
}
|
|
147
178
|
},
|
|
148
179
|
{
|
|
149
180
|
type: 'input',
|
|
150
181
|
name: 'user',
|
|
151
182
|
message: 'PostgreSQL 用户名:',
|
|
152
|
-
default: '
|
|
183
|
+
default: schemaDefault('pg', 'user')
|
|
153
184
|
},
|
|
154
185
|
{
|
|
155
186
|
type: 'password',
|
|
@@ -160,7 +191,7 @@ async function configurePostgreSQL(): Promise<DatabaseConfig> {
|
|
|
160
191
|
type: 'input',
|
|
161
192
|
name: 'database',
|
|
162
193
|
message: 'PostgreSQL 数据库名:',
|
|
163
|
-
default: '
|
|
194
|
+
default: schemaDefault('pg', 'database'),
|
|
164
195
|
validate: (input: string) => {
|
|
165
196
|
if (!input.trim()) return '数据库名不能为空';
|
|
166
197
|
return true;
|
|
@@ -171,7 +202,7 @@ async function configurePostgreSQL(): Promise<DatabaseConfig> {
|
|
|
171
202
|
return {
|
|
172
203
|
dialect: 'pg',
|
|
173
204
|
host: config.host,
|
|
174
|
-
port:
|
|
205
|
+
port: Number(config.port),
|
|
175
206
|
user: config.user,
|
|
176
207
|
password: config.password,
|
|
177
208
|
database: config.database
|
|
@@ -185,7 +216,7 @@ async function configureMongoDB(): Promise<DatabaseConfig> {
|
|
|
185
216
|
type: 'input',
|
|
186
217
|
name: 'url',
|
|
187
218
|
message: 'MongoDB 连接字符串:',
|
|
188
|
-
default: 'mongodb
|
|
219
|
+
default: schemaDefault('mongodb', 'url'),
|
|
189
220
|
validate: (input: string) => {
|
|
190
221
|
if (!input.trim()) return '连接字符串不能为空';
|
|
191
222
|
if (!input.startsWith('mongodb://') && !input.startsWith('mongodb+srv://')) {
|
|
@@ -198,7 +229,7 @@ async function configureMongoDB(): Promise<DatabaseConfig> {
|
|
|
198
229
|
type: 'input',
|
|
199
230
|
name: 'dbName',
|
|
200
231
|
message: 'MongoDB 数据库名:',
|
|
201
|
-
default: '
|
|
232
|
+
default: schemaDefault('mongodb', 'dbName'),
|
|
202
233
|
validate: (input: string) => {
|
|
203
234
|
if (!input.trim()) return '数据库名不能为空';
|
|
204
235
|
return true;
|
|
@@ -220,19 +251,16 @@ async function configureRedis(): Promise<DatabaseConfig> {
|
|
|
220
251
|
type: 'input',
|
|
221
252
|
name: 'host',
|
|
222
253
|
message: 'Redis 主机地址:',
|
|
223
|
-
default: '
|
|
254
|
+
default: schemaDefault('redis', 'socket', 'host')
|
|
224
255
|
},
|
|
225
256
|
{
|
|
226
257
|
type: 'input',
|
|
227
258
|
name: 'port',
|
|
228
259
|
message: 'Redis 端口:',
|
|
229
|
-
default: '
|
|
260
|
+
default: String(schemaDefault('redis', 'socket', 'port')),
|
|
230
261
|
validate: (input: string) => {
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
return '请输入有效的端口号 (1-65535)';
|
|
234
|
-
}
|
|
235
|
-
return true;
|
|
262
|
+
const value = parseIntegerInput(input, 1, 65535);
|
|
263
|
+
return typeof value === 'number' ? true : value;
|
|
236
264
|
}
|
|
237
265
|
},
|
|
238
266
|
{
|
|
@@ -244,13 +272,10 @@ async function configureRedis(): Promise<DatabaseConfig> {
|
|
|
244
272
|
type: 'input',
|
|
245
273
|
name: 'db',
|
|
246
274
|
message: 'Redis 数据库索引:',
|
|
247
|
-
default: '
|
|
275
|
+
default: String(schemaDefault('redis', 'database')),
|
|
248
276
|
validate: (input: string) => {
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
return '请输入有效的数据库索引 (>= 0)';
|
|
252
|
-
}
|
|
253
|
-
return true;
|
|
277
|
+
const value = parseIntegerInput(input, 0);
|
|
278
|
+
return typeof value === 'number' ? true : '请输入有效的数据库索引 (>= 0)';
|
|
254
279
|
}
|
|
255
280
|
}
|
|
256
281
|
]);
|
|
@@ -259,9 +284,9 @@ async function configureRedis(): Promise<DatabaseConfig> {
|
|
|
259
284
|
dialect: 'redis',
|
|
260
285
|
socket: {
|
|
261
286
|
host: config.host,
|
|
262
|
-
port:
|
|
287
|
+
port: Number(config.port)
|
|
263
288
|
},
|
|
264
289
|
password: config.password || undefined,
|
|
265
|
-
database:
|
|
290
|
+
database: Number(config.db)
|
|
266
291
|
};
|
|
267
|
-
}
|
|
292
|
+
}
|
package/src/env.ts
CHANGED
|
@@ -104,6 +104,7 @@ export function generateDatabaseEnvVars(config: DatabaseConfig): string {
|
|
|
104
104
|
);
|
|
105
105
|
break;
|
|
106
106
|
case 'sqlite':
|
|
107
|
+
case 'memory':
|
|
107
108
|
default:
|
|
108
109
|
// SQLite 不需要额外的环境变量
|
|
109
110
|
break;
|
|
@@ -111,3 +112,54 @@ export function generateDatabaseEnvVars(config: DatabaseConfig): string {
|
|
|
111
112
|
|
|
112
113
|
return envVars.length > 0 ? `\n\n${envVars.join('\n')}` : '';
|
|
113
114
|
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Generate a version-control-safe database section for `.env.example`.
|
|
118
|
+
* User-entered passwords and credential-bearing connection URLs are never
|
|
119
|
+
* copied from the interactive wizard.
|
|
120
|
+
*/
|
|
121
|
+
export function generateDatabaseEnvExample(config: DatabaseConfig): string {
|
|
122
|
+
let lines: string[] = [];
|
|
123
|
+
switch (config.dialect) {
|
|
124
|
+
case 'mysql':
|
|
125
|
+
lines = [
|
|
126
|
+
'# MySQL 数据库配置',
|
|
127
|
+
'DB_HOST=127.0.0.1',
|
|
128
|
+
'DB_PORT=3306',
|
|
129
|
+
'DB_USER=root',
|
|
130
|
+
'DB_PASSWORD=change-me',
|
|
131
|
+
'DB_DATABASE=zhin_bot',
|
|
132
|
+
];
|
|
133
|
+
break;
|
|
134
|
+
case 'pg':
|
|
135
|
+
lines = [
|
|
136
|
+
'# PostgreSQL 数据库配置',
|
|
137
|
+
'DB_HOST=127.0.0.1',
|
|
138
|
+
'DB_PORT=5432',
|
|
139
|
+
'DB_USER=postgres',
|
|
140
|
+
'DB_PASSWORD=change-me',
|
|
141
|
+
'DB_DATABASE=zhin_bot',
|
|
142
|
+
];
|
|
143
|
+
break;
|
|
144
|
+
case 'mongodb':
|
|
145
|
+
lines = [
|
|
146
|
+
'# MongoDB 数据库配置',
|
|
147
|
+
'DB_URL=mongodb://127.0.0.1:27017',
|
|
148
|
+
'DB_NAME=zhin_bot',
|
|
149
|
+
];
|
|
150
|
+
break;
|
|
151
|
+
case 'redis':
|
|
152
|
+
lines = [
|
|
153
|
+
'# Redis 数据库配置',
|
|
154
|
+
'REDIS_HOST=127.0.0.1',
|
|
155
|
+
'REDIS_PORT=6379',
|
|
156
|
+
'REDIS_PASSWORD=',
|
|
157
|
+
'REDIS_DB=0',
|
|
158
|
+
];
|
|
159
|
+
break;
|
|
160
|
+
case 'sqlite':
|
|
161
|
+
case 'memory':
|
|
162
|
+
return '';
|
|
163
|
+
}
|
|
164
|
+
return `\n\n${lines.join('\n')}`;
|
|
165
|
+
}
|