@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.
- package/lib/adapter.d.ts.map +1 -1
- package/lib/adapter.js +2 -1
- package/lib/adapter.js.map +1 -1
- package/lib/ai.d.ts +6 -0
- package/lib/ai.d.ts.map +1 -1
- package/lib/ai.js +64 -34
- package/lib/ai.js.map +1 -1
- package/lib/apply.js +2 -2
- package/lib/apply.js.map +1 -1
- package/lib/index.d.ts +10 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +5 -1
- package/lib/index.js.map +1 -1
- package/lib/optional-peers.d.ts +21 -0
- package/lib/optional-peers.d.ts.map +1 -0
- package/lib/optional-peers.js +105 -0
- package/lib/optional-peers.js.map +1 -0
- package/lib/project-config-plan.d.ts +53 -0
- package/lib/project-config-plan.d.ts.map +1 -0
- package/lib/project-config-plan.js +337 -0
- package/lib/project-config-plan.js.map +1 -0
- package/lib/project-deps.d.ts +26 -3
- package/lib/project-deps.d.ts.map +1 -1
- package/lib/project-deps.js +211 -26
- package/lib/project-deps.js.map +1 -1
- package/lib/upgrade-l4.d.ts +15 -0
- package/lib/upgrade-l4.d.ts.map +1 -0
- package/lib/upgrade-l4.js +68 -0
- package/lib/upgrade-l4.js.map +1 -0
- package/lib/zhin-stack-deps.d.ts +66 -0
- package/lib/zhin-stack-deps.d.ts.map +1 -0
- package/lib/zhin-stack-deps.js +202 -0
- package/lib/zhin-stack-deps.js.map +1 -0
- package/package.json +7 -5
- package/src/adapter.ts +2 -1
- package/src/ai.ts +76 -32
- package/src/apply.ts +2 -2
- package/src/index.ts +53 -0
- package/src/optional-peers.ts +149 -0
- package/src/project-config-plan.ts +400 -0
- package/src/project-deps.ts +228 -27
- package/src/stack-versions.generated.json +48 -0
- package/src/upgrade-l4.ts +103 -0
- package/src/zhin-stack-deps.ts +252 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import yaml from 'yaml';
|
|
4
|
+
import { parse as parseToml, stringify as stringifyToml } from 'smol-toml';
|
|
5
|
+
import { providerSdkFor } from './ai.js';
|
|
6
|
+
|
|
7
|
+
const CONSOLE_URL = 'https://console.zhin.dev';
|
|
8
|
+
const CONFIG_CANDIDATES = [
|
|
9
|
+
'zhin.config.yml',
|
|
10
|
+
'zhin.config.yaml',
|
|
11
|
+
'zhin.config.json',
|
|
12
|
+
'zhin.config.toml',
|
|
13
|
+
'zhin.config.ts',
|
|
14
|
+
] as const;
|
|
15
|
+
const CONSOLE_HOST_PLUGINS = ['@zhin.js/host-router', '@zhin.js/host-api'] as const;
|
|
16
|
+
const SANDBOX_PLUGIN = '@zhin.js/adapter-sandbox';
|
|
17
|
+
const LEGACY_AI_PROVIDER_FIELDS = ['driver', 'api', 'preset', 'spec'] as const;
|
|
18
|
+
|
|
19
|
+
export type ProjectConfigFormat = 'yaml' | 'json' | 'toml' | 'ts';
|
|
20
|
+
|
|
21
|
+
export interface LoadedProjectConfig {
|
|
22
|
+
status: 'loaded' | 'missing' | 'unsupported';
|
|
23
|
+
cwd: string;
|
|
24
|
+
configPath?: string;
|
|
25
|
+
relativePath?: string;
|
|
26
|
+
format?: ProjectConfigFormat;
|
|
27
|
+
config: Record<string, unknown>;
|
|
28
|
+
writable: boolean;
|
|
29
|
+
message?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ProjectConfigPlanOptions {
|
|
33
|
+
cwd?: string;
|
|
34
|
+
loaded?: LoadedProjectConfig;
|
|
35
|
+
configPath?: string;
|
|
36
|
+
config?: Record<string, unknown>;
|
|
37
|
+
format?: ProjectConfigFormat;
|
|
38
|
+
enablePlugins?: string[];
|
|
39
|
+
ensureConsole?: boolean;
|
|
40
|
+
ensureSandbox?: boolean;
|
|
41
|
+
ensureHttp?: boolean;
|
|
42
|
+
migrateAiLegacy?: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ProjectConfigPlan {
|
|
46
|
+
cwd: string;
|
|
47
|
+
status: LoadedProjectConfig['status'];
|
|
48
|
+
configPath?: string;
|
|
49
|
+
relativePath?: string;
|
|
50
|
+
format?: ProjectConfigFormat;
|
|
51
|
+
writable: boolean;
|
|
52
|
+
before: Record<string, unknown>;
|
|
53
|
+
after: Record<string, unknown>;
|
|
54
|
+
mutations: string[];
|
|
55
|
+
changed: boolean;
|
|
56
|
+
message?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface ConsoleConfigDiagnosis {
|
|
60
|
+
missingHostPlugins: string[];
|
|
61
|
+
missingSandboxPlugin: boolean;
|
|
62
|
+
missingConsoleOrigin: boolean;
|
|
63
|
+
missingHttpToken: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function configFormatFromPath(filePath: string): ProjectConfigFormat | null {
|
|
67
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
68
|
+
if (ext === '.yml' || ext === '.yaml') return 'yaml';
|
|
69
|
+
if (ext === '.json') return 'json';
|
|
70
|
+
if (ext === '.toml') return 'toml';
|
|
71
|
+
if (ext === '.ts') return 'ts';
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function cloneConfig(config: Record<string, unknown>): Record<string, unknown> {
|
|
76
|
+
return JSON.parse(JSON.stringify(config)) as Record<string, unknown>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function parseConfig(content: string, format: ProjectConfigFormat): Record<string, unknown> {
|
|
80
|
+
if (format === 'yaml') {
|
|
81
|
+
const parsed = yaml.parse(content) as unknown;
|
|
82
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
83
|
+
? parsed as Record<string, unknown>
|
|
84
|
+
: {};
|
|
85
|
+
}
|
|
86
|
+
if (format === 'json') {
|
|
87
|
+
const parsed = JSON.parse(content) as unknown;
|
|
88
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
89
|
+
? parsed as Record<string, unknown>
|
|
90
|
+
: {};
|
|
91
|
+
}
|
|
92
|
+
if (format === 'toml') {
|
|
93
|
+
const parsed = parseToml(content) as unknown;
|
|
94
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
95
|
+
? parsed as Record<string, unknown>
|
|
96
|
+
: {};
|
|
97
|
+
}
|
|
98
|
+
return {};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function serializeConfig(config: Record<string, unknown>, format: ProjectConfigFormat): string {
|
|
102
|
+
if (format === 'json') return `${JSON.stringify(config, null, 2)}\n`;
|
|
103
|
+
if (format === 'toml') return stringifyToml(config as Record<string, unknown>);
|
|
104
|
+
return yaml.stringify(config);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function findProjectConfigPath(cwd: string): string | null {
|
|
108
|
+
for (const candidate of CONFIG_CANDIDATES) {
|
|
109
|
+
const configPath = path.join(cwd, candidate);
|
|
110
|
+
if (fs.existsSync(configPath)) return configPath;
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function loadProjectConfig(cwd = process.cwd(), configPath?: string): LoadedProjectConfig {
|
|
116
|
+
const resolvedPath = configPath ? path.resolve(cwd, configPath) : findProjectConfigPath(cwd);
|
|
117
|
+
if (!resolvedPath) {
|
|
118
|
+
return {
|
|
119
|
+
status: 'missing',
|
|
120
|
+
cwd,
|
|
121
|
+
config: {},
|
|
122
|
+
writable: false,
|
|
123
|
+
message: '未找到 zhin.config.yml/json/toml',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const format = configFormatFromPath(resolvedPath);
|
|
128
|
+
const relativePath = path.relative(cwd, resolvedPath) || path.basename(resolvedPath);
|
|
129
|
+
if (!format) {
|
|
130
|
+
return {
|
|
131
|
+
status: 'unsupported',
|
|
132
|
+
cwd,
|
|
133
|
+
configPath: resolvedPath,
|
|
134
|
+
relativePath,
|
|
135
|
+
config: {},
|
|
136
|
+
writable: false,
|
|
137
|
+
message: `${path.basename(resolvedPath)} 暂不支持自动写入`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (format === 'ts') {
|
|
142
|
+
return {
|
|
143
|
+
status: 'unsupported',
|
|
144
|
+
cwd,
|
|
145
|
+
configPath: resolvedPath,
|
|
146
|
+
relativePath,
|
|
147
|
+
format,
|
|
148
|
+
config: {},
|
|
149
|
+
writable: false,
|
|
150
|
+
message: 'zhin.config.ts 只读;请迁移为 zhin.config.yml/json/toml 后自动修复',
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const content = fs.readFileSync(resolvedPath, 'utf8');
|
|
155
|
+
return {
|
|
156
|
+
status: 'loaded',
|
|
157
|
+
cwd,
|
|
158
|
+
configPath: resolvedPath,
|
|
159
|
+
relativePath,
|
|
160
|
+
format,
|
|
161
|
+
config: parseConfig(content, format),
|
|
162
|
+
writable: true,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function ensurePlugins(config: Record<string, unknown>, pluginsToAdd: readonly string[], mutations: string[]): void {
|
|
167
|
+
const plugins = Array.isArray(config.plugins)
|
|
168
|
+
? config.plugins.filter((p): p is string => typeof p === 'string')
|
|
169
|
+
: [];
|
|
170
|
+
let changed = false;
|
|
171
|
+
for (const plugin of pluginsToAdd) {
|
|
172
|
+
if (plugins.includes(plugin)) continue;
|
|
173
|
+
plugins.push(plugin);
|
|
174
|
+
mutations.push(`added ${plugin} to plugins`);
|
|
175
|
+
changed = true;
|
|
176
|
+
}
|
|
177
|
+
if (changed || !Array.isArray(config.plugins)) {
|
|
178
|
+
config.plugins = plugins;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function ensureHttp(config: Record<string, unknown>, mutations: string[]): void {
|
|
183
|
+
const http = config.http && typeof config.http === 'object' && !Array.isArray(config.http)
|
|
184
|
+
? { ...(config.http as Record<string, unknown>) }
|
|
185
|
+
: {};
|
|
186
|
+
const corsOrigins = Array.isArray(http.corsOrigins)
|
|
187
|
+
? http.corsOrigins.filter((origin): origin is string => typeof origin === 'string')
|
|
188
|
+
: [];
|
|
189
|
+
|
|
190
|
+
let changed = false;
|
|
191
|
+
if (!corsOrigins.includes(CONSOLE_URL)) {
|
|
192
|
+
corsOrigins.push(CONSOLE_URL);
|
|
193
|
+
http.corsOrigins = corsOrigins;
|
|
194
|
+
mutations.push(`added ${CONSOLE_URL} to http.corsOrigins`);
|
|
195
|
+
changed = true;
|
|
196
|
+
}
|
|
197
|
+
if (typeof http.token !== 'string' || http.token.trim().length === 0) {
|
|
198
|
+
http.token = '${HTTP_TOKEN}';
|
|
199
|
+
mutations.push('added http.token');
|
|
200
|
+
changed = true;
|
|
201
|
+
}
|
|
202
|
+
if (changed || !config.http) {
|
|
203
|
+
config.http = http;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function sdkFromLegacyProvider(alias: string, provider: Record<string, unknown>): string {
|
|
208
|
+
const candidates = LEGACY_AI_PROVIDER_FIELDS
|
|
209
|
+
.map((field) => provider[field])
|
|
210
|
+
.filter((value): value is string => typeof value === 'string' && value.trim().length > 0)
|
|
211
|
+
.map((value) => value.trim().toLowerCase());
|
|
212
|
+
const joined = candidates.join(' ');
|
|
213
|
+
if (joined.includes('ollama')) return 'ollama';
|
|
214
|
+
if (joined.includes('anthropic') || joined.includes('claude')) return 'anthropic';
|
|
215
|
+
if (joined.includes('google') || joined.includes('gemini')) return 'google';
|
|
216
|
+
if (joined.includes('deepseek')) return 'deepseek';
|
|
217
|
+
if (joined.includes('moonshot') || joined.includes('zhipu') || joined.includes('compatible')) {
|
|
218
|
+
return 'openai-compatible';
|
|
219
|
+
}
|
|
220
|
+
return providerSdkFor(alias);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function migrateAiLegacyConfig(ai: Record<string, unknown>): {
|
|
224
|
+
ai: Record<string, unknown>;
|
|
225
|
+
fixes: string[];
|
|
226
|
+
} {
|
|
227
|
+
const next = cloneConfig(ai);
|
|
228
|
+
const fixes: string[] = [];
|
|
229
|
+
const defaultProvider = typeof next.defaultProvider === 'string' && next.defaultProvider.trim()
|
|
230
|
+
? next.defaultProvider.trim()
|
|
231
|
+
: undefined;
|
|
232
|
+
|
|
233
|
+
const providers = next.providers && typeof next.providers === 'object' && !Array.isArray(next.providers)
|
|
234
|
+
? { ...(next.providers as Record<string, unknown>) }
|
|
235
|
+
: {};
|
|
236
|
+
let providersChanged = false;
|
|
237
|
+
|
|
238
|
+
for (const [alias, rawProvider] of Object.entries(providers)) {
|
|
239
|
+
if (!rawProvider || typeof rawProvider !== 'object' || Array.isArray(rawProvider)) continue;
|
|
240
|
+
const provider = { ...(rawProvider as Record<string, unknown>) };
|
|
241
|
+
const hasLegacyField = LEGACY_AI_PROVIDER_FIELDS.some((field) => field in provider);
|
|
242
|
+
if (!provider.sdk && hasLegacyField) {
|
|
243
|
+
provider.sdk = sdkFromLegacyProvider(alias, provider);
|
|
244
|
+
fixes.push(`migrated ai.providers.${alias}.driver/api to sdk`);
|
|
245
|
+
providersChanged = true;
|
|
246
|
+
}
|
|
247
|
+
for (const field of LEGACY_AI_PROVIDER_FIELDS) {
|
|
248
|
+
if (field in provider) {
|
|
249
|
+
delete provider[field];
|
|
250
|
+
providersChanged = true;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
providers[alias] = provider;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (providersChanged) {
|
|
257
|
+
next.providers = providers;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (defaultProvider) {
|
|
261
|
+
const agents = next.agents && typeof next.agents === 'object' && !Array.isArray(next.agents)
|
|
262
|
+
? { ...(next.agents as Record<string, unknown>) }
|
|
263
|
+
: {};
|
|
264
|
+
const zhin = agents.zhin && typeof agents.zhin === 'object' && !Array.isArray(agents.zhin)
|
|
265
|
+
? { ...(agents.zhin as Record<string, unknown>) }
|
|
266
|
+
: {};
|
|
267
|
+
if (!zhin.provider) {
|
|
268
|
+
zhin.provider = defaultProvider;
|
|
269
|
+
fixes.push('migrated ai.defaultProvider to ai.agents.zhin.provider');
|
|
270
|
+
}
|
|
271
|
+
const legacyAgent = next.agent && typeof next.agent === 'object' && !Array.isArray(next.agent)
|
|
272
|
+
? next.agent as Record<string, unknown>
|
|
273
|
+
: {};
|
|
274
|
+
const legacyModel = typeof legacyAgent.chatModel === 'string'
|
|
275
|
+
? legacyAgent.chatModel
|
|
276
|
+
: typeof legacyAgent.visionModel === 'string'
|
|
277
|
+
? legacyAgent.visionModel
|
|
278
|
+
: undefined;
|
|
279
|
+
if (legacyModel && !zhin.model) {
|
|
280
|
+
zhin.model = legacyModel;
|
|
281
|
+
fixes.push('migrated ai.agent.chatModel to ai.agents.zhin.model');
|
|
282
|
+
}
|
|
283
|
+
agents.zhin = zhin;
|
|
284
|
+
next.agents = agents;
|
|
285
|
+
delete next.defaultProvider;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return { ai: next, fixes };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function applyAiLegacyMigration(config: Record<string, unknown>, mutations: string[]): void {
|
|
292
|
+
if (!config.ai || typeof config.ai !== 'object' || Array.isArray(config.ai)) return;
|
|
293
|
+
const { ai, fixes } = migrateAiLegacyConfig(config.ai as Record<string, unknown>);
|
|
294
|
+
if (fixes.length === 0) return;
|
|
295
|
+
config.ai = ai;
|
|
296
|
+
mutations.push(...fixes);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export function createProjectConfigPlan(options: ProjectConfigPlanOptions): ProjectConfigPlan {
|
|
300
|
+
const cwd = options.cwd ?? options.loaded?.cwd ?? process.cwd();
|
|
301
|
+
const loaded = options.loaded ?? (
|
|
302
|
+
options.config
|
|
303
|
+
? {
|
|
304
|
+
status: 'loaded' as const,
|
|
305
|
+
cwd,
|
|
306
|
+
configPath: options.configPath,
|
|
307
|
+
relativePath: options.configPath ? path.relative(cwd, options.configPath) : undefined,
|
|
308
|
+
format: options.format ?? (options.configPath ? configFormatFromPath(options.configPath) ?? undefined : undefined),
|
|
309
|
+
config: options.config,
|
|
310
|
+
writable: Boolean(options.configPath && (options.format ?? configFormatFromPath(options.configPath)) !== 'ts'),
|
|
311
|
+
}
|
|
312
|
+
: loadProjectConfig(cwd, options.configPath)
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
const sourceConfig = options.config ?? loaded.config;
|
|
316
|
+
const before = cloneConfig(sourceConfig);
|
|
317
|
+
const after = cloneConfig(sourceConfig);
|
|
318
|
+
const mutations: string[] = [];
|
|
319
|
+
|
|
320
|
+
if (loaded.status === 'loaded' || options.config) {
|
|
321
|
+
if (options.ensureConsole) {
|
|
322
|
+
ensurePlugins(after, CONSOLE_HOST_PLUGINS, mutations);
|
|
323
|
+
}
|
|
324
|
+
if (options.ensureSandbox) {
|
|
325
|
+
ensurePlugins(after, [SANDBOX_PLUGIN], mutations);
|
|
326
|
+
}
|
|
327
|
+
if (options.enablePlugins?.length) {
|
|
328
|
+
ensurePlugins(after, options.enablePlugins, mutations);
|
|
329
|
+
}
|
|
330
|
+
if (options.ensureHttp) {
|
|
331
|
+
ensureHttp(after, mutations);
|
|
332
|
+
}
|
|
333
|
+
if (options.migrateAiLegacy) {
|
|
334
|
+
applyAiLegacyMigration(after, mutations);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
cwd,
|
|
340
|
+
status: loaded.status,
|
|
341
|
+
configPath: loaded.configPath,
|
|
342
|
+
relativePath: loaded.relativePath,
|
|
343
|
+
format: loaded.format,
|
|
344
|
+
writable: loaded.writable,
|
|
345
|
+
before,
|
|
346
|
+
after,
|
|
347
|
+
mutations,
|
|
348
|
+
changed: mutations.length > 0,
|
|
349
|
+
message: loaded.message,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export function renderProjectConfigPatch(plan: ProjectConfigPlan): string {
|
|
354
|
+
if (!plan.changed) return '配置无需改动。';
|
|
355
|
+
const format = plan.format && plan.format !== 'ts' ? plan.format : 'yaml';
|
|
356
|
+
const name = plan.relativePath ?? 'zhin.config.yml';
|
|
357
|
+
return [
|
|
358
|
+
`# ${name}`,
|
|
359
|
+
serializeConfig(plan.after, format).trimEnd(),
|
|
360
|
+
'',
|
|
361
|
+
].join('\n');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export async function applyProjectConfigPlan(plan: ProjectConfigPlan): Promise<boolean> {
|
|
365
|
+
if (!plan.changed) return false;
|
|
366
|
+
if (!plan.writable || !plan.configPath || !plan.format || plan.format === 'ts') return false;
|
|
367
|
+
await fs.writeFile(plan.configPath, serializeConfig(plan.after, plan.format));
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
export function diagnoseConsoleConfig(config: Record<string, unknown>): ConsoleConfigDiagnosis {
|
|
372
|
+
const plugins = Array.isArray(config.plugins)
|
|
373
|
+
? config.plugins.filter((p): p is string => typeof p === 'string')
|
|
374
|
+
: [];
|
|
375
|
+
const http = config.http && typeof config.http === 'object' && !Array.isArray(config.http)
|
|
376
|
+
? config.http as Record<string, unknown>
|
|
377
|
+
: {};
|
|
378
|
+
const corsOrigins = Array.isArray(http.corsOrigins) ? http.corsOrigins : [];
|
|
379
|
+
|
|
380
|
+
return {
|
|
381
|
+
missingHostPlugins: CONSOLE_HOST_PLUGINS.filter((plugin) => !plugins.includes(plugin)),
|
|
382
|
+
missingSandboxPlugin: !plugins.includes(SANDBOX_PLUGIN),
|
|
383
|
+
missingConsoleOrigin: !corsOrigins.includes(CONSOLE_URL),
|
|
384
|
+
missingHttpToken: typeof http.token !== 'string' || http.token.trim().length === 0,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export function applyConsoleConfigFixes(config: Record<string, unknown>): boolean {
|
|
389
|
+
const plan = createProjectConfigPlan({
|
|
390
|
+
cwd: process.cwd(),
|
|
391
|
+
config,
|
|
392
|
+
ensureConsole: true,
|
|
393
|
+
ensureSandbox: true,
|
|
394
|
+
ensureHttp: true,
|
|
395
|
+
migrateAiLegacy: true,
|
|
396
|
+
});
|
|
397
|
+
Object.keys(config).forEach((key) => delete config[key]);
|
|
398
|
+
Object.assign(config, plan.after);
|
|
399
|
+
return plan.changed;
|
|
400
|
+
}
|