@zhin.js/scaffold-wizard 1.1.1 → 1.1.3

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/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
- { name: 'SQLite (推荐, 零配置)', value: 'sqlite' },
13
- { name: 'MySQL', value: 'mysql' },
14
- { name: 'PostgreSQL', value: 'pg' },
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
- return await configureSQLite();
48
+ config = await configureSQLite();
49
+ break;
25
50
  case 'mysql':
26
- return await configureMySQL();
51
+ config = await configureMySQL();
52
+ break;
27
53
  case 'pg':
28
- return await configurePostgreSQL();
54
+ config = await configurePostgreSQL();
55
+ break;
29
56
  case 'mongodb':
30
- return await configureMongoDB();
57
+ config = await configureMongoDB();
58
+ break;
31
59
  case 'redis':
32
- return await configureRedis();
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: './data/bot.db',
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: 'wal'
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: 'localhost'
115
+ default: schemaDefault('mysql', 'host')
79
116
  },
80
117
  {
81
118
  type: 'input',
82
119
  name: 'port',
83
120
  message: 'MySQL 端口:',
84
- default: '3306',
121
+ default: String(schemaDefault('mysql', 'port')),
85
122
  validate: (input: string) => {
86
- const port = parseInt(input);
87
- if (isNaN(port) || port < 1 || port > 65535) {
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: 'root'
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: 'zhin_bot',
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: parseInt(config.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: 'localhost'
167
+ default: schemaDefault('pg', 'host')
134
168
  },
135
169
  {
136
170
  type: 'input',
137
171
  name: 'port',
138
172
  message: 'PostgreSQL 端口:',
139
- default: '5432',
173
+ default: String(schemaDefault('pg', 'port')),
140
174
  validate: (input: string) => {
141
- const port = parseInt(input);
142
- if (isNaN(port) || port < 1 || port > 65535) {
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: 'postgres'
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: 'zhin_bot',
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: parseInt(config.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://localhost:27017',
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: 'zhin_bot',
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: 'localhost'
254
+ default: schemaDefault('redis', 'socket', 'host')
224
255
  },
225
256
  {
226
257
  type: 'input',
227
258
  name: 'port',
228
259
  message: 'Redis 端口:',
229
- default: '6379',
260
+ default: String(schemaDefault('redis', 'socket', 'port')),
230
261
  validate: (input: string) => {
231
- const port = parseInt(input);
232
- if (isNaN(port) || port < 1 || port > 65535) {
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: '0',
275
+ default: String(schemaDefault('redis', 'database')),
248
276
  validate: (input: string) => {
249
- const db = parseInt(input);
250
- if (isNaN(db) || db < 0) {
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: parseInt(config.port)
287
+ port: Number(config.port)
263
288
  },
264
289
  password: config.password || undefined,
265
- database: parseInt(config.db)
290
+ database: Number(config.db)
266
291
  };
267
- }
292
+ }
@@ -0,0 +1,132 @@
1
+ import inquirer from 'inquirer';
2
+ import chalk from 'chalk';
3
+ import fs from 'fs-extra';
4
+ import path from 'node:path';
5
+ import { formatEnvValue, mergeEnvText } from './env.js';
6
+ import { clonePluginConfigurationMap, mergeDependenciesIntoPackageJson, mergePluginManifestIntoPackageJson } from './apply.js';
7
+ import { isAiEnabledInConfig } from './project-deps.js';
8
+
9
+ export const TYPESAFE_DECISION_PACKAGE = '@zhin.js/service-typesafe';
10
+ export interface TypeSafeSetupConfig {
11
+ readonly instanceKey: string;
12
+ readonly apiKey: string;
13
+ readonly model: string;
14
+ readonly mode: 'shadow' | 'active';
15
+ readonly envVar?: string;
16
+ }
17
+ function hasChatAgent(config: Readonly<Record<string, unknown>>): boolean {
18
+ if (!isAiEnabledInConfig(config)) return false;
19
+ const binding = record(record(record(config.ai).agents).zhin);
20
+ return typeof binding.provider === 'string' && !!binding.provider.trim()
21
+ && typeof binding.model === 'string' && !!binding.model.trim();
22
+ }
23
+ function record(value: unknown): Record<string, unknown> {
24
+ return value && typeof value === 'object' && !Array.isArray(value) ? value as Record<string, unknown> : {};
25
+ }
26
+
27
+ /** An explicitly requested optional integration. Existing AI configuration is preserved. */
28
+ export async function configureTypeSafeDecisions(config: Readonly<Record<string, unknown>>): Promise<TypeSafeSetupConfig> {
29
+ if (!hasChatAgent(config)) throw new Error('先运行 zhin setup --ai 配置聊天模型,再运行 zhin setup --decisions。Jev 决策模型不能替代聊天模型。');
30
+ console.log(chalk.blue('\nTypeSafe Jev:选择 Agent / Skill / Tool 的可选决策服务。'));
31
+ console.log(chalk.gray('shadow 仅记录建议,不改变已有行为;active 启用推荐。审核默认关闭。调用会向外部服务发送经过权限过滤的请求与候选描述。'));
32
+ const answers = await inquirer.prompt([
33
+ { type: 'input', name: 'instanceKey', message: '决策插件实例名', default: 'typesafe', validate: (value: string) => /^[a-z][a-z0-9-]*$/.test(value) || '使用小写字母、数字和连字符,字母开头' },
34
+ { type: 'password', name: 'apiKey', message: 'TypeSafe API Key(仅写入 .env)', mask: '*', validate: (value: string) => value.trim().length > 0 || '请输入 API Key' },
35
+ { type: 'input', name: 'model', message: 'Jev 模型(生产校准后建议固定版本)', default: 'jev-latest', validate: (value: string) => value.trim().length > 0 || '请输入模型名称' },
36
+ { type: 'select', name: 'mode', message: 'Skill / Tool 推荐运行方式', default: 'shadow', choices: [ { name: 'shadow:观察建议,评估后再启用', value: 'shadow' }, { name: 'active:启用推荐,权限仍由 Zhin 校验', value: 'active' } ] },
37
+ ]);
38
+ const reference = record(record(config.plugins)[answers.instanceKey]).apiKey;
39
+ const existingVariable = typeof reference === 'string' ? /^\$\{([A-Za-z_][A-Za-z0-9_]*)\}$/.exec(reference)?.[1] : undefined;
40
+ return { ...answers, envVar: existingVariable ?? decisionKeyVariable(answers.instanceKey) };
41
+ }
42
+
43
+ function decisionKeyVariable(instanceKey: string): string {
44
+ return instanceKey === 'typesafe' ? 'TYPESAFE_API_KEY' : `TYPESAFE_${instanceKey.toUpperCase().replace(/-/g, '_')}_API_KEY`;
45
+ }
46
+ function envVariable(setup: TypeSafeSetupConfig): string {
47
+ const name = setup.envVar ?? decisionKeyVariable(setup.instanceKey);
48
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(name)) throw new Error('Invalid TypeSafe environment variable name.');
49
+ return name;
50
+ }
51
+
52
+ /** Plan data separately from writes so secrets cannot enter the configuration document. */
53
+ export function applyTypeSafeDecisionsToConfig(config: Record<string, unknown>, setup: TypeSafeSetupConfig): void {
54
+ if (!hasChatAgent(config)) throw new Error('TypeSafe decisions require existing AI configuration; run zhin setup --ai first.');
55
+ if (!/^[a-z][a-z0-9-]*$/.test(setup.instanceKey) || !setup.model.trim() || !setup.apiKey.trim()) throw new Error('Invalid TypeSafe setup configuration.');
56
+ const plugins = clonePluginConfigurationMap(config);
57
+ const existing = record(plugins[setup.instanceKey]);
58
+ plugins[setup.instanceKey] = { ...existing, apiKey: '${' + envVariable(setup) + '}', model: setup.model.trim(), timeoutMs: 10000, maxRetries: 2, enabled: true };
59
+ config.plugins = plugins;
60
+ const ai = record(config.ai);
61
+ const previousDecisions = record(ai.decisions);
62
+ config.ai = { ...ai, decisions: {
63
+ ...previousDecisions,
64
+ provider: `root/${setup.instanceKey}`,
65
+ skills: { ...record(previousDecisions.skills), mode: setup.mode },
66
+ tools: { ...record(previousDecisions.tools), mode: setup.mode },
67
+ agents: previousDecisions.agents ?? { mode: 'off' },
68
+ memory: previousDecisions.memory ?? { mode: 'off' },
69
+ approval: previousDecisions.approval ?? { mode: 'off' },
70
+ } };
71
+ }
72
+
73
+ export async function saveTypeSafeSetupDependencies(projectDir: string, setup: TypeSafeSetupConfig): Promise<void> {
74
+ const variable = envVariable(setup);
75
+ const pkgPath = path.join(projectDir, 'package.json');
76
+ const pkg = await fs.readJson(pkgPath);
77
+ const existingManifest = record(pkg.zhin).plugins;
78
+ if (Array.isArray(existingManifest) && existingManifest.some(entry => record(entry).instanceKey === setup.instanceKey && record(entry).package !== TYPESAFE_DECISION_PACKAGE)) {
79
+ throw new Error('Decision plugin instance already belongs to another package; choose a different instanceKey.');
80
+ }
81
+ await mergePluginManifestIntoPackageJson(projectDir, [{ package: TYPESAFE_DECISION_PACKAGE, instanceKey: setup.instanceKey }]);
82
+ await mergeDependenciesIntoPackageJson(projectDir, { [TYPESAFE_DECISION_PACKAGE]: '^1.0.0' });
83
+ const envPath = path.join(projectDir, '.env');
84
+ const envExists = await fs.pathExists(envPath);
85
+ const previous = envExists ? await fs.readFile(envPath, 'utf8') : '';
86
+ // writeFile's mode applies only when creating a file. Restrict existing files
87
+ // before adding credentials; Windows access control is not POSIX file modes.
88
+ if (envExists && process.platform !== 'win32') await fs.chmod(envPath, 0o600);
89
+ await fs.writeFile(envPath, mergeEnvText(previous, `${variable}=${formatEnvValue(setup.apiKey)}\n`), { mode: 0o600 });
90
+ }
91
+
92
+ /** Local checks only; never sends a paid API request or returns credential values. */
93
+ export function diagnoseDecisionConfig(config: Readonly<Record<string, unknown>>, packageJson: unknown): string[] {
94
+ const ai = record(config.ai);
95
+ if (!Object.hasOwn(ai, 'decisions')) return [];
96
+ const decisions = record(ai.decisions);
97
+ const issues: string[] = [];
98
+ const taskNames = ['skills', 'tools', 'memory', 'agents', 'approval'];
99
+ if (Object.keys(decisions).some(key => !['provider', ...taskNames].includes(key))) issues.push('ai.decisions 含有未知配置字段。');
100
+ const provider = decisions.provider;
101
+ if (typeof provider !== 'string' || !/^root(?:\/[a-z0-9][a-z0-9-]*)+$/.test(provider)) return [...issues, 'ai.decisions.provider 必须是精确插件 owner,例如 root/typesafe。'];
102
+ for (const name of taskNames) {
103
+ if (!Object.hasOwn(decisions, name)) continue;
104
+ const task = record(decisions[name]);
105
+ if (Object.keys(task).some(key => !['mode', 'timeoutMs', 'minConfidence', 'topK', 'maxCandidates', 'maxSelections'].includes(key))) issues.push(`ai.decisions.${name} 含有未知配置字段。`);
106
+ if (typeof task.mode !== 'string' || !['off', 'shadow', 'active'].includes(task.mode)) issues.push(`ai.decisions.${name}.mode 必须是 off、shadow 或 active。`);
107
+ for (const field of ['timeoutMs', 'topK', 'maxCandidates', 'maxSelections']) {
108
+ if (task[field] !== undefined && (!Number.isSafeInteger(task[field]) || Number(task[field]) <= 0 || field === 'timeoutMs' && Number(task[field]) > 2147483647)) issues.push(`ai.decisions.${name}.${field} 必须是正整数。`);
109
+ }
110
+ if (task.minConfidence !== undefined && (typeof task.minConfidence !== 'number' || !Number.isFinite(task.minConfidence) || task.minConfidence < 0 || task.minConfidence > 1)) issues.push(`ai.decisions.${name}.minConfidence 必须在 0 到 1 之间。`);
111
+ }
112
+ if (!hasChatAgent(config)) issues.push('决策服务需要已启用的 AI Agent;运行 zhin setup --ai。');
113
+ const enabled = taskNames.some(name => ['shadow', 'active'].includes(String(record(decisions[name]).mode)));
114
+ if (!enabled) return issues;
115
+ const key = provider.slice('root/'.length);
116
+ // Nested Plugin owners are resolved from the actual runtime graph during activation.
117
+ if (key.includes('/')) return issues;
118
+ const pkg = record(packageJson);
119
+ const manifest = record(pkg.zhin).plugins;
120
+ const entries = Array.isArray(manifest) ? manifest.filter(entry => record(entry).instanceKey === key) : [];
121
+ if (entries.length !== 1) {
122
+ issues.push('决策插件实例未在 package.json#zhin.plugins 唯一声明。');
123
+ return issues;
124
+ }
125
+ if (record(entries[0]).package === TYPESAFE_DECISION_PACKAGE) {
126
+ const configForProvider = record(record(config.plugins)[key]);
127
+ if (configForProvider.enabled === false) issues.push('绑定的 TypeSafe 插件已禁用。');
128
+ if (typeof configForProvider.apiKey !== 'string' || !configForProvider.apiKey.trim()) issues.push('TypeSafe 插件缺少 apiKey 环境变量引用。');
129
+ if (!Object.hasOwn({ ...record(pkg.dependencies), ...record(pkg.devDependencies) }, TYPESAFE_DECISION_PACKAGE)) issues.push('package.json 缺少 @zhin.js/service-typesafe 依赖。');
130
+ }
131
+ return issues;
132
+ }
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
+ }
package/src/index.ts CHANGED
@@ -1,5 +1,17 @@
1
1
  export type { InitOptions, DatabaseConfig } from './types.js';
2
- export { DATABASE_PACKAGES } from './types.js';
2
+ export { DATABASE_DRIVER_VERSIONS, DATABASE_PACKAGES } from './types.js';
3
+ export {
4
+ DATABASE_DIALECT_DEFINITIONS,
5
+ databaseChoiceLabel,
6
+ getDatabaseDialectDefinition,
7
+ validateDatabaseConfig,
8
+ } from './database-definitions.js';
9
+ export type {
10
+ DatabaseDialect,
11
+ DatabaseDialectDefinition,
12
+ DatabaseDriverDefinition,
13
+ DatabaseSchemaIssue,
14
+ } from './database-definitions.js';
3
15
 
4
16
  export type { AdapterSetupResult, AdapterPluginInstance, AdapterDefinition } from './adapter.js';
5
17
  export {
@@ -35,6 +47,7 @@ export { configureDatabaseOptions } from './database.js';
35
47
  export {
36
48
  formatEnvValue,
37
49
  mergeEnvText,
50
+ generateDatabaseEnvExample,
38
51
  generateDatabaseEnvVars,
39
52
  } from './env.js';
40
53
 
@@ -60,9 +73,9 @@ export type { AiStackIncompatibility } from './project-deps.js';
60
73
  export {
61
74
  ZHIN_STACK_VERSIONS,
62
75
  DEFAULT_CREATE_BOT_HTTP_PORT,
63
- CREATE_BOT_NPMRC,
76
+ CREATE_BOT_PACKAGE_MANAGER,
64
77
  getCreateBotBaseDependencies,
65
- getCreateBotPnpmConfig,
78
+ getCreateBotPnpmWorkspaceConfig,
66
79
  getRequiredZhinDependenciesForConfig,
67
80
  diagnoseZhinStackDependencies,
68
81
  formatZhinStackFixCommand,
@@ -123,3 +136,9 @@ export {
123
136
  buildRuntimeConfigDocument,
124
137
  serializeRuntimeConfig,
125
138
  } from './apply.js';
139
+
140
+ export {
141
+ TYPESAFE_DECISION_PACKAGE, configureTypeSafeDecisions, applyTypeSafeDecisionsToConfig,
142
+ saveTypeSafeSetupDependencies, diagnoseDecisionConfig,
143
+ } from './decisions.js';
144
+ export type { TypeSafeSetupConfig } from './decisions.js';
@@ -7,7 +7,6 @@
7
7
  "@zhin.js/database": "^1.0.77",
8
8
  "@zhin.js/client": "^2.1.0",
9
9
  "@zhin.js/contract": "^1.0.5",
10
- "@zhin.js/satori": "^1.0.16",
11
10
  "@zhin.js/speech": "^2.0.0",
12
11
  "@zhin.js/html-renderer": "^2.0.0",
13
12
  "@zhin.js/mcp": "^5.0.0",
package/src/types.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { AISetupConfig } from './ai.js';
2
2
  import type { AdapterSetupResult } from './adapter.js';
3
+ import { DATABASE_DIALECT_DEFINITIONS } from './database-definitions.js';
3
4
 
4
5
  export type { AISetupConfig } from './ai.js';
5
6
  export type { AdapterSetupResult } from './adapter.js';
@@ -19,15 +20,17 @@ export interface InitOptions {
19
20
  }
20
21
 
21
22
  export interface DatabaseConfig {
22
- dialect: 'sqlite' | 'mysql' | 'pg' | 'mongodb' | 'redis';
23
+ dialect: 'sqlite' | 'mysql' | 'pg' | 'mongodb' | 'redis' | 'memory';
23
24
  [key: string]: any;
24
25
  }
25
26
 
26
- // 数据库配置映射(SQLite 使用 Node 内置 node:sqlite,无需额外安装)
27
- export const DATABASE_PACKAGES: Record<DatabaseConfig['dialect'], string | undefined> = {
28
- sqlite: undefined,
29
- mysql: 'mysql2',
30
- pg: 'pg',
31
- mongodb: 'mongodb',
32
- redis: 'redis'
33
- };
27
+ // 数据库驱动映射(SQLite / Memory 使用内置实现,无需额外安装)
28
+ export const DATABASE_PACKAGES = Object.freeze(Object.fromEntries(
29
+ DATABASE_DIALECT_DEFINITIONS.map((definition) => [definition.dialect, definition.driver?.package]),
30
+ )) as Readonly<Record<DatabaseConfig['dialect'], string | undefined>>;
31
+
32
+ export const DATABASE_DRIVER_VERSIONS = Object.freeze(Object.fromEntries(
33
+ DATABASE_DIALECT_DEFINITIONS.flatMap((definition) => definition.driver
34
+ ? [[definition.driver.package, definition.driver.version] as const]
35
+ : []),
36
+ )) as Readonly<Record<string, string>>;
@@ -1,12 +1,13 @@
1
1
  import { createRequire } from 'node:module';
2
2
  import fs from 'fs-extra';
3
3
  import path from 'node:path';
4
- import { DATABASE_PACKAGES } from './types.js';
4
+ import { DATABASE_DRIVER_VERSIONS, DATABASE_PACKAGES } from './types.js';
5
5
  import { findMissingPackageDependencies, findUnresolvedPackageInstalls, isAiEnabledInConfig, type PackageJsonLike } from './project-deps.js';
6
6
 
7
7
  /**
8
- * User project dependency policy: scaffolded dependencies intentionally float to latest.
9
- * WARNING: this is not reproducible; pin versions for production. See scaffold-wizard README.
8
+ * User project dependency policy: scaffolded Zhin packages intentionally float to latest.
9
+ * Database drivers use the live-tested ranges declared by the database definitions.
10
+ * WARNING: `latest` is not reproducible; pin Zhin package versions for production.
10
11
  */
11
12
  export const ZHIN_STACK_VERSIONS = {
12
13
  'zhin.js': 'latest',
@@ -27,7 +28,7 @@ export const ZHIN_STACK_VERSIONS = {
27
28
  '@zhin.js/tool': 'latest',
28
29
  '@zhin.js/client': 'latest',
29
30
  '@zhin.js/contract': 'latest',
30
- '@zhin.js/satori': 'latest',
31
+ '@zhin.js/components': 'latest',
31
32
  '@zhin.js/speech': 'latest',
32
33
  '@zhin.js/html-renderer': 'latest',
33
34
  '@zhin.js/mcp': 'latest',
@@ -114,13 +115,17 @@ function resolveStackVersion(packageName: string): string {
114
115
  /** create-zhin / minimal-bot 默认 Host 端口(避免与常见 8086 占用冲突) */
115
116
  export const DEFAULT_CREATE_BOT_HTTP_PORT = 8068;
116
117
 
117
- /** create-zhin 生成项目的 .npmrc(避免全局 strict-peer-dependencies 阻断 AI 栈安装) */
118
- export const CREATE_BOT_NPMRC = 'strict-peer-dependencies=false\n';
118
+ /** create-zhin 生成项目固定使用的 pnpm 版本。 */
119
+ export const CREATE_BOT_PACKAGE_MANAGER = 'pnpm@11.27.1';
119
120
 
120
- /** create-zhin 生成项目的 pnpm 配置 */
121
- export function getCreateBotPnpmConfig(_aiEnabled?: boolean): Record<string, unknown> {
121
+ /** create-zhin 生成项目的 pnpm-workspace.yaml 配置。 */
122
+ export function getCreateBotPnpmWorkspaceConfig(): Record<string, unknown> {
122
123
  return {
123
- onlyBuiltDependencies: ['esbuild'],
124
+ packages: ['.', 'plugins/*', 'packages/*'],
125
+ strictPeerDependencies: false,
126
+ allowBuilds: {
127
+ esbuild: true,
128
+ },
124
129
  };
125
130
  }
126
131
 
@@ -147,7 +152,7 @@ export function getRequiredZhinDependenciesForConfig(config: Record<string, unkn
147
152
  const dialect = String((database as Record<string, unknown>).dialect ?? '');
148
153
  const driver = DATABASE_PACKAGES[dialect as keyof typeof DATABASE_PACKAGES];
149
154
  if (driver) {
150
- deps[driver] = 'latest';
155
+ deps[driver] = DATABASE_DRIVER_VERSIONS[driver];
151
156
  }
152
157
  }
153
158