@zhin.js/scaffold-wizard 0.1.5 → 0.1.7

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.
Files changed (44) hide show
  1. package/lib/adapter.d.ts.map +1 -1
  2. package/lib/adapter.js +2 -1
  3. package/lib/adapter.js.map +1 -1
  4. package/lib/ai.d.ts +6 -0
  5. package/lib/ai.d.ts.map +1 -1
  6. package/lib/ai.js +64 -34
  7. package/lib/ai.js.map +1 -1
  8. package/lib/apply.js +2 -2
  9. package/lib/apply.js.map +1 -1
  10. package/lib/index.d.ts +10 -1
  11. package/lib/index.d.ts.map +1 -1
  12. package/lib/index.js +5 -1
  13. package/lib/index.js.map +1 -1
  14. package/lib/optional-peers.d.ts +21 -0
  15. package/lib/optional-peers.d.ts.map +1 -0
  16. package/lib/optional-peers.js +105 -0
  17. package/lib/optional-peers.js.map +1 -0
  18. package/lib/project-config-plan.d.ts +53 -0
  19. package/lib/project-config-plan.d.ts.map +1 -0
  20. package/lib/project-config-plan.js +337 -0
  21. package/lib/project-config-plan.js.map +1 -0
  22. package/lib/project-deps.d.ts +26 -3
  23. package/lib/project-deps.d.ts.map +1 -1
  24. package/lib/project-deps.js +211 -26
  25. package/lib/project-deps.js.map +1 -1
  26. package/lib/upgrade-l4.d.ts +15 -0
  27. package/lib/upgrade-l4.d.ts.map +1 -0
  28. package/lib/upgrade-l4.js +68 -0
  29. package/lib/upgrade-l4.js.map +1 -0
  30. package/lib/zhin-stack-deps.d.ts +66 -0
  31. package/lib/zhin-stack-deps.d.ts.map +1 -0
  32. package/lib/zhin-stack-deps.js +202 -0
  33. package/lib/zhin-stack-deps.js.map +1 -0
  34. package/package.json +7 -5
  35. package/src/adapter.ts +2 -1
  36. package/src/ai.ts +76 -32
  37. package/src/apply.ts +2 -2
  38. package/src/index.ts +53 -0
  39. package/src/optional-peers.ts +149 -0
  40. package/src/project-config-plan.ts +400 -0
  41. package/src/project-deps.ts +228 -27
  42. package/src/stack-versions.generated.json +48 -0
  43. package/src/upgrade-l4.ts +103 -0
  44. package/src/zhin-stack-deps.ts +252 -0
@@ -4,22 +4,62 @@ import path from 'node:path';
4
4
  import type { AISetupConfig } from './ai.js';
5
5
  import type { InitOptions } from './types.js';
6
6
 
7
- /** 与 monorepo 中 agent / test-bot 对齐的 MCP SDK 版本 */
8
- export const MCP_SDK_VERSION = '^1.29.0';
7
+ /**
8
+ * User project dependency policy: scaffolded dependencies intentionally float to latest.
9
+ */
10
+ export const AI_STACK_VERSIONS = {
11
+ '@zhin.js/agent': 'latest',
12
+ zod: 'latest',
13
+ ai: 'latest',
14
+ '@modelcontextprotocol/sdk': 'latest',
15
+ '@ai-sdk/openai': 'latest',
16
+ '@ai-sdk/anthropic': 'latest',
17
+ '@ai-sdk/google': 'latest',
18
+ '@ai-sdk/deepseek': 'latest',
19
+ '@ai-sdk/openai-compatible': 'latest',
20
+ } as const;
9
21
 
10
- const AI_STACK_VERSION = '^4.0.0';
11
- const ZOD_VERSION = '^4.0.0';
12
- const VERCEL_AI_VERSION = '^6.0.0';
22
+ /** @deprecated 使用 AI_STACK_VERSIONS['@modelcontextprotocol/sdk'] */
23
+ export const MCP_SDK_VERSION = AI_STACK_VERSIONS['@modelcontextprotocol/sdk'];
24
+
25
+ const WIZARD_ALIAS_TO_SDK: Record<string, string> = {
26
+ openai: 'openai',
27
+ anthropic: 'anthropic',
28
+ deepseek: 'deepseek',
29
+ google: 'google',
30
+ gemini: 'google',
31
+ ollama: 'ollama',
32
+ moonshot: 'openai-compatible',
33
+ zhipu: 'openai-compatible',
34
+ };
35
+
36
+ const SDK_TO_NPM_PACKAGE: Record<string, keyof typeof AI_STACK_VERSIONS> = {
37
+ openai: '@ai-sdk/openai',
38
+ anthropic: '@ai-sdk/anthropic',
39
+ google: '@ai-sdk/google',
40
+ deepseek: '@ai-sdk/deepseek',
41
+ ollama: '@ai-sdk/openai-compatible',
42
+ 'openai-compatible': '@ai-sdk/openai-compatible',
43
+ };
13
44
 
14
45
  const PROVIDER_SDK_PACKAGES: Record<string, { pkg: string; version: string }> = {
15
- openai: { pkg: '@ai-sdk/openai', version: '^3.0.0' },
16
- anthropic: { pkg: '@ai-sdk/anthropic', version: '^3.0.0' },
17
- google: { pkg: '@ai-sdk/google', version: '^3.0.0' },
18
- gemini: { pkg: '@ai-sdk/google', version: '^3.0.0' },
19
- deepseek: { pkg: '@ai-sdk/deepseek', version: '^1.0.0' },
20
- ollama: { pkg: '@ai-sdk/openai-compatible', version: '^1.0.0' },
21
- moonshot: { pkg: '@ai-sdk/openai-compatible', version: '^1.0.0' },
22
- zhipu: { pkg: '@ai-sdk/openai-compatible', version: '^1.0.0' },
46
+ openai: { pkg: '@ai-sdk/openai', version: AI_STACK_VERSIONS['@ai-sdk/openai'] },
47
+ anthropic: { pkg: '@ai-sdk/anthropic', version: AI_STACK_VERSIONS['@ai-sdk/anthropic'] },
48
+ google: { pkg: '@ai-sdk/google', version: AI_STACK_VERSIONS['@ai-sdk/google'] },
49
+ gemini: { pkg: '@ai-sdk/google', version: AI_STACK_VERSIONS['@ai-sdk/google'] },
50
+ deepseek: { pkg: '@ai-sdk/deepseek', version: AI_STACK_VERSIONS['@ai-sdk/deepseek'] },
51
+ ollama: { pkg: '@ai-sdk/openai-compatible', version: AI_STACK_VERSIONS['@ai-sdk/openai-compatible'] },
52
+ moonshot: { pkg: '@ai-sdk/openai-compatible', version: AI_STACK_VERSIONS['@ai-sdk/openai-compatible'] },
53
+ zhipu: { pkg: '@ai-sdk/openai-compatible', version: AI_STACK_VERSIONS['@ai-sdk/openai-compatible'] },
54
+ };
55
+
56
+ /** @ai-sdk/* 主版本 ≥ threshold 时需 ai@7 */
57
+ const AI7_REQUIRED_SDK_MAJOR: Record<string, number> = {
58
+ '@ai-sdk/openai-compatible': 3,
59
+ '@ai-sdk/openai': 4,
60
+ '@ai-sdk/anthropic': 4,
61
+ '@ai-sdk/google': 4,
62
+ '@ai-sdk/deepseek': 3,
23
63
  };
24
64
 
25
65
  function providerSdkPackage(provider?: string): { pkg: string; version: string } | undefined {
@@ -27,6 +67,147 @@ function providerSdkPackage(provider?: string): { pkg: string; version: string }
27
67
  return PROVIDER_SDK_PACKAGES[provider];
28
68
  }
29
69
 
70
+ function inferSdkId(providerAlias: string, entry?: { sdk?: string }): string | undefined {
71
+ const explicit = entry?.sdk?.trim();
72
+ if (explicit) return explicit;
73
+ if (WIZARD_ALIAS_TO_SDK[providerAlias]) return WIZARD_ALIAS_TO_SDK[providerAlias];
74
+ if (SDK_TO_NPM_PACKAGE[providerAlias]) return providerAlias;
75
+ return undefined;
76
+ }
77
+
78
+ function packagesForSdkIds(sdkIds: Iterable<string>): Record<string, string> {
79
+ const deps: Record<string, string> = {};
80
+ for (const sdkId of sdkIds) {
81
+ const npmName = SDK_TO_NPM_PACKAGE[sdkId];
82
+ if (!npmName) continue;
83
+ deps[npmName] = AI_STACK_VERSIONS[npmName];
84
+ }
85
+ return deps;
86
+ }
87
+
88
+ function collectSdkPackagesFromSetup(ai: AISetupConfig): Record<string, string> {
89
+ const sdkIds = new Set<string>();
90
+ const agentProvider = ai.agentProvider ?? ai.defaultProvider;
91
+ if (agentProvider) {
92
+ const id = inferSdkId(agentProvider, ai.providers?.[agentProvider] as { sdk?: string });
93
+ if (id) sdkIds.add(id);
94
+ }
95
+ for (const [alias, entry] of Object.entries(ai.providers ?? {})) {
96
+ const id = inferSdkId(alias, entry as { sdk?: string });
97
+ if (id) sdkIds.add(id);
98
+ }
99
+ return packagesForSdkIds(sdkIds);
100
+ }
101
+
102
+ function collectSdkPackagesFromConfig(config: { ai?: unknown }): Record<string, string> {
103
+ const ai = config.ai;
104
+ if (!ai || typeof ai !== 'object' || Array.isArray(ai)) return {};
105
+ const rec = ai as Record<string, unknown>;
106
+ const providers = rec.providers as Record<string, { sdk?: string }> | undefined;
107
+ const sdkIds = new Set<string>();
108
+
109
+ if (providers) {
110
+ for (const [alias, entry] of Object.entries(providers)) {
111
+ const id = inferSdkId(alias, entry);
112
+ if (id) sdkIds.add(id);
113
+ }
114
+ }
115
+
116
+ const agents = rec.agents as Record<string, { provider?: string }> | undefined;
117
+ if (agents && providers) {
118
+ for (const agent of Object.values(agents)) {
119
+ if (!agent?.provider) continue;
120
+ const id = inferSdkId(agent.provider, providers[agent.provider]);
121
+ if (id) sdkIds.add(id);
122
+ }
123
+ }
124
+
125
+ if (sdkIds.size === 0) {
126
+ const alias = resolveDefaultProviderFromConfig(config);
127
+ if (alias) {
128
+ const id = inferSdkId(alias, providers?.[alias]);
129
+ if (id) sdkIds.add(id);
130
+ }
131
+ }
132
+
133
+ return packagesForSdkIds(sdkIds);
134
+ }
135
+
136
+ function parseMajor(versionSpec: string | undefined): number | undefined {
137
+ if (!versionSpec?.trim()) return undefined;
138
+ const match = versionSpec.trim().replace(/^[\^~>=<]+/, '').match(/^(\d+)/);
139
+ return match ? Number(match[1]) : undefined;
140
+ }
141
+
142
+ function getDeclaredPackageVersion(pkg: PackageJsonLike, name: string): string | undefined {
143
+ return pkg.dependencies?.[name] ?? pkg.devDependencies?.[name];
144
+ }
145
+
146
+ function readInstalledMajor(cwd: string, packageName: string): number | undefined {
147
+ const pkgJson = path.join(cwd, 'package.json');
148
+ if (!fs.existsSync(pkgJson)) return undefined;
149
+ try {
150
+ const req = createRequire(pkgJson);
151
+ const installed = req(`${packageName}/package.json`) as { version?: string };
152
+ return parseMajor(installed.version);
153
+ } catch {
154
+ return undefined;
155
+ }
156
+ }
157
+
158
+ function resolvePackageMajor(cwd: string, pkg: PackageJsonLike, packageName: string): number | undefined {
159
+ return readInstalledMajor(cwd, packageName) ?? parseMajor(getDeclaredPackageVersion(pkg, packageName));
160
+ }
161
+
162
+ export interface AiStackIncompatibility {
163
+ package: string;
164
+ reason: string;
165
+ }
166
+
167
+ export function findOutdatedAiStackInPackageJson(
168
+ pkg: PackageJsonLike,
169
+ required: Record<string, string>,
170
+ ): string[] {
171
+ const outdated: string[] = [];
172
+ for (const [name, minSpec] of Object.entries(required)) {
173
+ const declared = getDeclaredPackageVersion(pkg, name);
174
+ if (!declared) continue;
175
+ const declaredMajor = parseMajor(declared);
176
+ const minMajor = parseMajor(minSpec);
177
+ if (declaredMajor != null && minMajor != null && declaredMajor < minMajor) {
178
+ outdated.push(name);
179
+ }
180
+ }
181
+ return outdated;
182
+ }
183
+
184
+ export function findInstalledAiStackIncompatibilities(
185
+ cwd: string,
186
+ pkg: PackageJsonLike,
187
+ ): AiStackIncompatibility[] {
188
+ const aiMajor = resolvePackageMajor(cwd, pkg, 'ai');
189
+ if (aiMajor == null || aiMajor >= 7) return [];
190
+
191
+ const issues: AiStackIncompatibility[] = [];
192
+ if (aiMajor < 7) {
193
+ issues.push({
194
+ package: 'ai',
195
+ reason: 'ai@6 与 @ai-sdk/openai-compatible@3(v4 model spec)不兼容,请升级 ai 至 ^7.0.0',
196
+ });
197
+ }
198
+
199
+ for (const [name, threshold] of Object.entries(AI7_REQUIRED_SDK_MAJOR)) {
200
+ const major = resolvePackageMajor(cwd, pkg, name);
201
+ if (major == null || major < threshold) continue;
202
+ issues.push({
203
+ package: name,
204
+ reason: `${name}@${major} 需要 ai@7(当前 ai@${aiMajor})`,
205
+ });
206
+ }
207
+
208
+ return issues;
209
+ }
210
+
30
211
  /** 启用 AI 时将写入 package.json 的依赖名(用于向导提示与 CLI 输出) */
31
212
  export function listAIDependencyNames(provider?: string, options?: { includeMcp?: boolean }): string[] {
32
213
  const names = ['@zhin.js/agent', 'zod', 'ai'];
@@ -99,10 +280,14 @@ export function getRequiredAIDependenciesForConfig(config: { ai?: unknown }): Re
99
280
  const ai = config.ai as Record<string, unknown>;
100
281
  const setup: AISetupConfig = {
101
282
  enabled: true,
102
- defaultProvider: resolveDefaultProviderFromConfig(config),
283
+ agentProvider: resolveDefaultProviderFromConfig(config),
284
+ providers: ai.providers as AISetupConfig['providers'],
103
285
  memoryMcp: aiConfigNeedsMcp(ai),
104
286
  };
105
- const deps = getAIDependencies(setup);
287
+ const deps = {
288
+ ...getAIDependencies(setup),
289
+ ...collectSdkPackagesFromConfig(config),
290
+ };
106
291
  if (!aiConfigNeedsMcp(ai)) {
107
292
  delete deps['@modelcontextprotocol/sdk'];
108
293
  }
@@ -140,9 +325,25 @@ export interface AIDependencyDiagnosis {
140
325
  provider?: string;
141
326
  required: Record<string, string>;
142
327
  missingFromPackageJson: string[];
328
+ outdatedInPackageJson: string[];
329
+ incompatibleInstalled: AiStackIncompatibility[];
143
330
  notInstalled: string[];
144
331
  }
145
332
 
333
+ export function packagesNeedingAiStackFix(diagnosis: AIDependencyDiagnosis): string[] {
334
+ const names = new Set<string>([
335
+ ...diagnosis.missingFromPackageJson,
336
+ ...diagnosis.outdatedInPackageJson,
337
+ ]);
338
+ for (const issue of diagnosis.incompatibleInstalled) {
339
+ names.add(issue.package);
340
+ }
341
+ if (diagnosis.incompatibleInstalled.some((issue) => issue.package !== 'ai')) {
342
+ names.add('ai');
343
+ }
344
+ return [...names];
345
+ }
346
+
146
347
  export function diagnoseAIDependencies(
147
348
  cwd: string,
148
349
  config: { ai?: unknown },
@@ -153,6 +354,8 @@ export function diagnoseAIDependencies(
153
354
  const packageJsonPath = path.join(cwd, 'package.json');
154
355
  const packageJson = pkg ?? (fs.existsSync(packageJsonPath) ? fs.readJsonSync(packageJsonPath) as PackageJsonLike : {});
155
356
  const missingFromPackageJson = findMissingPackageDependencies(packageJson, required);
357
+ const outdatedInPackageJson = findOutdatedAiStackInPackageJson(packageJson, required);
358
+ const incompatibleInstalled = findInstalledAiStackIncompatibilities(cwd, packageJson);
156
359
  const declared = Object.keys(required).filter((name) => !missingFromPackageJson.includes(name));
157
360
  const notInstalled = findUnresolvedPackageInstalls(cwd, declared);
158
361
  return {
@@ -160,13 +363,15 @@ export function diagnoseAIDependencies(
160
363
  provider: resolveDefaultProviderFromConfig(config),
161
364
  required,
162
365
  missingFromPackageJson,
366
+ outdatedInPackageJson,
367
+ incompatibleInstalled,
163
368
  notInstalled,
164
369
  };
165
370
  }
166
371
 
167
- export function formatAIDependencyFixCommand(missing: string[], required: Record<string, string>): string {
168
- if (missing.length === 0) return 'pnpm install';
169
- const specs = missing.map((name) => `${name}@${required[name] ?? 'latest'}`);
372
+ export function formatAIDependencyFixCommand(packages: string[], required: Record<string, string>): string {
373
+ if (packages.length === 0) return 'pnpm install';
374
+ const specs = packages.map((name) => `${name}@${required[name] ?? 'latest'}`);
170
375
  return `pnpm add ${specs.join(' ')}`;
171
376
  }
172
377
 
@@ -177,17 +382,13 @@ export function getAIDependencies(ai?: AISetupConfig): Record<string, string> {
177
382
  if (!ai?.enabled) return {};
178
383
 
179
384
  const deps: Record<string, string> = {
180
- '@zhin.js/agent': AI_STACK_VERSION,
181
- zod: ZOD_VERSION,
182
- ai: VERCEL_AI_VERSION,
183
- '@modelcontextprotocol/sdk': MCP_SDK_VERSION,
385
+ '@zhin.js/agent': AI_STACK_VERSIONS['@zhin.js/agent'],
386
+ zod: AI_STACK_VERSIONS.zod,
387
+ ai: AI_STACK_VERSIONS.ai,
388
+ '@modelcontextprotocol/sdk': AI_STACK_VERSIONS['@modelcontextprotocol/sdk'],
389
+ ...collectSdkPackagesFromSetup(ai),
184
390
  };
185
391
 
186
- const sdk = providerSdkPackage(ai.defaultProvider);
187
- if (sdk) {
188
- deps[sdk.pkg] = sdk.version;
189
- }
190
-
191
392
  return deps;
192
393
  }
193
394
 
@@ -0,0 +1,48 @@
1
+ {
2
+ "$comment": "Auto-generated by scripts/sync-scaffold-deps.mjs — do not edit; run pnpm sync:scaffold-deps",
3
+ "zhinStack": {
4
+ "zhin.js": "^4.1.0",
5
+ "@zhin.js/cli": "^1.0.91",
6
+ "@zhin.js/agent": "^1.0.1",
7
+ "@zhin.js/database": "^1.0.74",
8
+ "@zhin.js/host-router": "^2.0.1",
9
+ "@zhin.js/host-api": "^2.0.3",
10
+ "@zhin.js/client": "^2.0.3",
11
+ "@zhin.js/contract": "^1.0.1",
12
+ "@zhin.js/satori": "^1.0.15",
13
+ "@zhin.js/speech": "^1.0.1",
14
+ "@zhin.js/html-renderer": "^1.0.1",
15
+ "@zhin.js/mcp": "^4.0.0",
16
+ "@zhin.js/adapter-sandbox": "^5.0.3",
17
+ "@zhin.js/adapter-dingtalk": "^4.0.0",
18
+ "@zhin.js/adapter-discord": "^5.0.0",
19
+ "@zhin.js/adapter-email": "^3.0.0",
20
+ "@zhin.js/adapter-github": "^3.0.0",
21
+ "@zhin.js/adapter-icqq": "^6.0.0",
22
+ "@zhin.js/adapter-kook": "^5.0.0",
23
+ "@zhin.js/adapter-lark": "^4.0.0",
24
+ "@zhin.js/adapter-line": "^2.0.0",
25
+ "@zhin.js/adapter-milky": "^3.0.0",
26
+ "@zhin.js/adapter-napcat": "^3.0.0",
27
+ "@zhin.js/adapter-onebot11": "^4.0.0",
28
+ "@zhin.js/adapter-onebot12": "^3.0.0",
29
+ "@zhin.js/adapter-qq": "^5.0.0",
30
+ "@zhin.js/adapter-satori": "^3.0.0",
31
+ "@zhin.js/adapter-slack": "^4.0.0",
32
+ "@zhin.js/adapter-telegram": "^5.0.0",
33
+ "@zhin.js/adapter-wechat-mp": "^3.0.0",
34
+ "@zhin.js/adapter-wecom": "^2.0.0",
35
+ "@zhin.js/adapter-weixin-ilink": "^3.0.0"
36
+ },
37
+ "aiStack": {
38
+ "@zhin.js/agent": "^1.0.1",
39
+ "ai": "^7.0.0",
40
+ "zod": "^4.0.0",
41
+ "@ai-sdk/openai": "^4.0.0",
42
+ "@ai-sdk/anthropic": "^4.0.0",
43
+ "@ai-sdk/google": "^4.0.0",
44
+ "@ai-sdk/deepseek": "^3.0.0",
45
+ "@ai-sdk/openai-compatible": "^3.0.0",
46
+ "@modelcontextprotocol/sdk": "^1.29.0"
47
+ }
48
+ }
@@ -0,0 +1,103 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'node:path';
3
+ import type { PackageJsonLike } from './project-deps.js';
4
+ import {
5
+ diagnoseAIDependencies,
6
+ formatAIDependencyFixCommand,
7
+ getRequiredAIDependenciesForConfig,
8
+ isAiEnabledInConfig,
9
+ findMissingPackageDependencies,
10
+ findUnresolvedPackageInstalls,
11
+ packagesNeedingAiStackFix,
12
+ } from './project-deps.js';
13
+ import { diagnoseOptionalPeers, formatOptionalPeerFixCommand, getOptionalPeerDependencies } from './optional-peers.js';
14
+
15
+ export interface UpgradeToL4Diagnosis {
16
+ ai: ReturnType<typeof diagnoseAIDependencies>;
17
+ optionalPeers: ReturnType<typeof diagnoseOptionalPeers>;
18
+ missingAiDeps: string[];
19
+ missingOptionalPeers: string[];
20
+ fixCommand: string;
21
+ configSnippets: string[];
22
+ }
23
+
24
+ const L4_CONFIG_SNIPPETS = [
25
+ '# --- L4 可选多模态(取消注释并安装 optional peer)---',
26
+ '# ai:',
27
+ '# multimodal:',
28
+ '# audio:',
29
+ '# strategy: transcribe # 需 @zhin.js/speech',
30
+ '# speech:',
31
+ '# stt:',
32
+ '# provider: ollama',
33
+ '# model: whisper',
34
+ '# tts:',
35
+ '# provider: edge',
36
+ '# htmlRenderer:',
37
+ '# width: 540',
38
+ ];
39
+
40
+ /** minimal-bot → full-bot / L4 升级诊断 */
41
+ export function diagnoseUpgradeToL4(
42
+ cwd: string,
43
+ config: Record<string, unknown>,
44
+ pkg?: PackageJsonLike,
45
+ ): UpgradeToL4Diagnosis {
46
+ const packageJsonPath = path.join(cwd, 'package.json');
47
+ const packageJson = pkg ?? (fs.existsSync(packageJsonPath)
48
+ ? fs.readJsonSync(packageJsonPath) as PackageJsonLike
49
+ : {});
50
+
51
+ const aiEnabled = isAiEnabledInConfig(config);
52
+ const ai = aiEnabled ? diagnoseAIDependencies(cwd, config, packageJson) : null;
53
+ const optionalPeers = diagnoseOptionalPeers(cwd, config, packageJson);
54
+
55
+ const missingAiDeps = ai
56
+ ? [...new Set([...packagesNeedingAiStackFix(ai), ...ai.notInstalled])]
57
+ : [...Object.keys(getRequiredAIDependenciesForConfig({ ai: { enabled: true } }))];
58
+
59
+ if (!aiEnabled) {
60
+ const required = getRequiredAIDependenciesForConfig({ ai: { enabled: true } });
61
+ const fromPkg = findMissingPackageDependencies(packageJson, required);
62
+ const declared = Object.keys(required).filter((n) => !fromPkg.includes(n));
63
+ const notInstalled = findUnresolvedPackageInstalls(cwd, declared);
64
+ missingAiDeps.push(...fromPkg, ...notInstalled);
65
+ }
66
+
67
+ const missingOptionalPeers: string[] = [];
68
+ for (const peer of [optionalPeers.speech, optionalPeers.htmlRenderer]) {
69
+ if (!peer?.required) continue;
70
+ missingOptionalPeers.push(
71
+ ...peer.missingFromPackageJson,
72
+ ...peer.notInstalled.filter((n) => !peer.missingFromPackageJson.includes(n)),
73
+ );
74
+ }
75
+
76
+ const fixParts: string[] = [];
77
+ if (!aiEnabled || missingAiDeps.length > 0) {
78
+ const required = ai?.required ?? getRequiredAIDependenciesForConfig({ ai: { enabled: true } });
79
+ fixParts.push(formatAIDependencyFixCommand(
80
+ [...new Set(missingAiDeps)],
81
+ required,
82
+ ));
83
+ }
84
+ for (const peer of [optionalPeers.speech, optionalPeers.htmlRenderer]) {
85
+ if (peer && (peer.missingFromPackageJson.length > 0 || peer.notInstalled.length > 0)) {
86
+ fixParts.push(formatOptionalPeerFixCommand(peer));
87
+ }
88
+ }
89
+
90
+ return {
91
+ ai,
92
+ optionalPeers,
93
+ missingAiDeps: [...new Set(missingAiDeps)],
94
+ missingOptionalPeers: [...new Set(missingOptionalPeers)],
95
+ fixCommand: fixParts.join(' && '),
96
+ configSnippets: L4_CONFIG_SNIPPETS,
97
+ };
98
+ }
99
+
100
+ export function getUpgradeToL4Dependencies(config: Record<string, unknown>): Record<string, string> {
101
+ const deps = getRequiredAIDependenciesForConfig({ ai: { enabled: true } });
102
+ return { ...deps, ...getOptionalPeerDependencies(config) };
103
+ }