mister-wolf 2.20.0 → 2.21.0
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 +1 -1
- package/README.ru.md +1 -1
- package/dist/adapters/platforms/index.d.ts.map +1 -1
- package/dist/adapters/platforms/index.js +2 -1
- package/dist/adapters/platforms/index.js.map +1 -1
- package/dist/adapters/platforms/opencode-adapter.d.ts +49 -7
- package/dist/adapters/platforms/opencode-adapter.d.ts.map +1 -1
- package/dist/adapters/platforms/opencode-adapter.js +540 -102
- package/dist/adapters/platforms/opencode-adapter.js.map +1 -1
- package/dist/adapters/platforms/opencode-config-keys.d.ts +110 -0
- package/dist/adapters/platforms/opencode-config-keys.d.ts.map +1 -0
- package/dist/adapters/platforms/opencode-config-keys.js +214 -0
- package/dist/adapters/platforms/opencode-config-keys.js.map +1 -0
- package/dist/app/use-cases/doctor.d.ts.map +1 -1
- package/dist/app/use-cases/doctor.js +6 -0
- package/dist/app/use-cases/doctor.js.map +1 -1
- package/dist/app/use-cases/init-project.d.ts.map +1 -1
- package/dist/app/use-cases/init-project.js +8 -3
- package/dist/app/use-cases/init-project.js.map +1 -1
- package/dist/app/use-cases/repair-platform.d.ts.map +1 -1
- package/dist/app/use-cases/repair-platform.js +4 -1
- package/dist/app/use-cases/repair-platform.js.map +1 -1
- package/dist/app/use-cases/tool-librarian.d.ts.map +1 -1
- package/dist/app/use-cases/tool-librarian.js +3 -1
- package/dist/app/use-cases/tool-librarian.js.map +1 -1
- package/dist/app/use-cases/uninstall.js +1 -1
- package/dist/app/use-cases/uninstall.js.map +1 -1
- package/dist/ports/platform-adapter.port.d.ts +19 -4
- package/dist/ports/platform-adapter.port.d.ts.map +1 -1
- package/package.json +1 -1
- package/templates/.wolf/router.log +1 -1
- package/update-map.json +3 -3
|
@@ -2,14 +2,22 @@ import * as fs from 'fs/promises';
|
|
|
2
2
|
import { existsSync } from 'fs';
|
|
3
3
|
import { execFile } from 'child_process';
|
|
4
4
|
import { promisify } from 'util';
|
|
5
|
-
import { basename, join } from 'path';
|
|
5
|
+
import { basename, join, relative } from 'path';
|
|
6
6
|
import { parseJsonc } from './jsonc.js';
|
|
7
7
|
import { writeFileAtomic } from '../fs/markdown-memory-store.js';
|
|
8
8
|
import { UserFacingError } from '../../domain/errors.js';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
export
|
|
9
|
+
import { parseStamp } from '../render/stamp.js';
|
|
10
|
+
import yaml from 'js-yaml';
|
|
11
|
+
import { CONFIG_LOCATIONS, DEFAULT_AGENT, DEFAULT_AGENT_KEY, MCP_KEY, MCP_ROOT, MCP_SERVERS, SUBAGENT_DEPTH, SUBAGENT_DEPTH_KEY, WOLF_MCP_COMMAND, WRITE_LOCATION, WRITE_LOCATION_ID, deletePath, getPath, getRecordAt, isWolfMcpEntry, legacyPathOf, blockedSegment, locatePath, mcpEntryNative, mergeSurface, pathWritable, relPathOf, setPath, } from './opencode-config-keys.js';
|
|
12
|
+
export { DEFAULT_AGENT, SUBAGENT_DEPTH };
|
|
13
|
+
/** Каноническая команда wolf: removeWolf/configIssues не получают cmd, ownership проверяем ею. */
|
|
14
|
+
const WOLF_CMD = WOLF_MCP_COMMAND;
|
|
15
|
+
/** Значение глубины в действующем месте — наше или совместимое (>= SUBAGENT_DEPTH). */
|
|
16
|
+
function depthIsOurs(value) {
|
|
17
|
+
return typeof value === 'number' && value >= SUBAGENT_DEPTH;
|
|
18
|
+
}
|
|
19
|
+
const dotted = (key) => key.writePath.join('.');
|
|
20
|
+
const leaf = (key) => key.writePath[key.writePath.length - 1];
|
|
13
21
|
const execFileAsync = promisify(execFile);
|
|
14
22
|
/** Минимальная поддержанная v1 (порог диапазона). */
|
|
15
23
|
const V1_MIN_MINOR = 18;
|
|
@@ -17,6 +25,9 @@ const V1_MIN_PATCH = 29;
|
|
|
17
25
|
/**
|
|
18
26
|
* Чистый парсер `opencode --version`. Эмпирика форматов: v1 печатает `1.18.35`,
|
|
19
27
|
* v2 — `opencode v2.0.24`; regex вытаскивает M.m.p из обоих.
|
|
28
|
+
*
|
|
29
|
+
* Нативная форма `mcp.servers.<name>` принимается opencode >= 1.18.29 (v2-compat hoist),
|
|
30
|
+
* а порог v1 в коде — ровно 1.18.29: ветвление по версии для выбора формы не нужно.
|
|
20
31
|
*/
|
|
21
32
|
export function parseOpencodeVersion(raw) {
|
|
22
33
|
const trimmed = raw.trim();
|
|
@@ -50,126 +61,560 @@ export class OpencodeAdapter {
|
|
|
50
61
|
return { status: 'absent', message: err instanceof Error ? err.message : String(err) };
|
|
51
62
|
}
|
|
52
63
|
}
|
|
53
|
-
/**
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
64
|
+
/**
|
|
65
|
+
* Файл локации: jsonc — только если json отсутствует (правило прежнее),
|
|
66
|
+
* иначе json. Если файла нет — возвращается дефолтный путь (цель записи).
|
|
67
|
+
*/
|
|
68
|
+
locationFile(projectRoot, location) {
|
|
69
|
+
const dir = location.dir === '' ? projectRoot : join(projectRoot, location.dir);
|
|
70
|
+
const jsonc = join(dir, `${location.fileBase}.jsonc`);
|
|
71
|
+
const json = join(dir, `${location.fileBase}.json`);
|
|
72
|
+
return existsSync(jsonc) && !existsSync(json) ? jsonc : json;
|
|
73
|
+
}
|
|
74
|
+
/** Корневой конфиг — единственное место, куда wolf пишет свои ключи. */
|
|
75
|
+
rootFile(projectRoot) {
|
|
76
|
+
return this.locationFile(projectRoot, WRITE_LOCATION);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Поверхность конфига (6.7): обе локации + эффективный (слейдженный) вид.
|
|
80
|
+
* Битый/нечитаемый файл ИЗОЛИРУЕТСЯ: он попадает в `errors`, а остальные локации
|
|
81
|
+
* читаются как обычно (прежнее поведение «всё или ничего» чинило в обратную сторону).
|
|
82
|
+
*/
|
|
83
|
+
async loadSurface(projectRoot) {
|
|
84
|
+
const files = [];
|
|
85
|
+
const errors = [];
|
|
86
|
+
for (const location of CONFIG_LOCATIONS) {
|
|
87
|
+
const file = this.locationFile(projectRoot, location);
|
|
88
|
+
if (!existsSync(file))
|
|
89
|
+
continue;
|
|
90
|
+
const rel = relative(projectRoot, file);
|
|
91
|
+
let raw;
|
|
92
|
+
try {
|
|
93
|
+
raw = await fs.readFile(file, 'utf-8');
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
errors.push({
|
|
97
|
+
location: location.id,
|
|
98
|
+
file,
|
|
99
|
+
rel,
|
|
100
|
+
kind: 'not readable',
|
|
101
|
+
message: `${file} is not readable: ${err instanceof Error ? err.message : err}`,
|
|
102
|
+
});
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
let parsed;
|
|
106
|
+
try {
|
|
107
|
+
parsed = parseJsonc(raw);
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
// паритет с ClaudeAdapter: ошибки парсинга — UserFacingError, не сырой SyntaxError
|
|
111
|
+
errors.push({
|
|
112
|
+
location: location.id,
|
|
113
|
+
file,
|
|
114
|
+
rel,
|
|
115
|
+
kind: 'not valid JSONC',
|
|
116
|
+
message: `${file} is not valid JSONC: ${err instanceof Error ? err.message : err}`,
|
|
117
|
+
});
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const cfg = asRecord(parsed);
|
|
121
|
+
if (cfg === undefined) {
|
|
122
|
+
errors.push({
|
|
123
|
+
location: location.id,
|
|
124
|
+
file,
|
|
125
|
+
rel,
|
|
126
|
+
kind: 'not a JSON object',
|
|
127
|
+
message: `${file} is not a JSON object — refusing to touch it`,
|
|
128
|
+
});
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
files.push({ location: location.id, file, rel, cfg });
|
|
57
132
|
}
|
|
58
|
-
return
|
|
133
|
+
return { files, errors, effective: mergeSurface(files) };
|
|
134
|
+
}
|
|
135
|
+
/** Битая/нечитаемая корневая локация (та, куда wolf пишет) — прежний контракт: ошибка наружу. */
|
|
136
|
+
static rootError(surface) {
|
|
137
|
+
return surface.errors.find((error) => error.location === WRITE_LOCATION_ID);
|
|
59
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Эффективный конфиг обеих локаций; null — конфиг-файлов нет ни в одной локации.
|
|
141
|
+
* Битый корневой файл — UserFacingError (прежний контракт для корня);
|
|
142
|
+
* битый `.opencode` — не мешает: возвращаем вид читаемых файлов.
|
|
143
|
+
*/
|
|
60
144
|
async readConfig(projectRoot) {
|
|
61
|
-
const
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
catch {
|
|
145
|
+
const surface = await this.loadSurface(projectRoot);
|
|
146
|
+
const rootError = OpencodeAdapter.rootError(surface);
|
|
147
|
+
if (rootError !== undefined)
|
|
148
|
+
throw new UserFacingError(rootError.message);
|
|
149
|
+
if (surface.files.length === 0)
|
|
67
150
|
return null;
|
|
151
|
+
return surface.effective;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Запись wolf-ключей (6.7): решения принимаются по эффективному виду обеих
|
|
155
|
+
* локаций, запись — только в корневой конфиг (в .opencode wolf не пишет).
|
|
156
|
+
*
|
|
157
|
+
* opts.createFile=false — режим repair-only: файл конфига не создаётся, если его нет
|
|
158
|
+
* (минимальный init чинит существующий конфиг и никогда не создаёт платформенный).
|
|
159
|
+
*/
|
|
160
|
+
async writeConfig(projectRoot, cmd, opts) {
|
|
161
|
+
const surface = await this.loadSurface(projectRoot);
|
|
162
|
+
// битый/нечитаемый корень — прежнее поведение: файл не трогаем, ошибка наружу
|
|
163
|
+
const rootError = OpencodeAdapter.rootError(surface);
|
|
164
|
+
if (rootError !== undefined)
|
|
165
|
+
throw new UserFacingError(rootError.message);
|
|
166
|
+
const file = this.rootFile(projectRoot);
|
|
167
|
+
const rootExists = surface.files.some((f) => f.location === WRITE_LOCATION_ID);
|
|
168
|
+
if (!rootExists && opts?.createFile === false) {
|
|
169
|
+
// минимальный init: конфига нет — не создаём (документированное обещание)
|
|
170
|
+
const other = surface.files.find((f) => f.location !== WRITE_LOCATION_ID);
|
|
171
|
+
const reason = other !== undefined
|
|
172
|
+
? `no root ${relPathOf(WRITE_LOCATION)} to repair — the project config is ${other.rel}; a minimal init never creates a platform config (use wolf init --platform ${this.id})`
|
|
173
|
+
: `no root ${relPathOf(WRITE_LOCATION)} to repair — a minimal init never creates a platform config (use wolf init --platform ${this.id})`;
|
|
174
|
+
return { action: 'skipped', reason };
|
|
68
175
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
176
|
+
const rootCfg = rootConfigOf(surface);
|
|
177
|
+
// 6.3: пишем нативную форму mcp.servers.wolf, легаси mcp.wolf снимаем только если он наш
|
|
178
|
+
const desired = mcpEntryNative(cmd);
|
|
179
|
+
const legacyMcpPath = legacyPathOf(MCP_KEY);
|
|
180
|
+
const nativeAt = locatePath(surface.files, MCP_KEY.writePath);
|
|
181
|
+
const legacyAt = locatePath(surface.files, legacyMcpPath);
|
|
182
|
+
const legacyMcpOurs = legacyAt !== undefined && isWolfMcpEntry(legacyAt.value, cmd);
|
|
183
|
+
const mcpWritable = pathWritable(rootCfg, MCP_KEY.writePath);
|
|
184
|
+
const blockedReasons = [];
|
|
185
|
+
const agentReasons = [];
|
|
186
|
+
const depthReasons = [];
|
|
187
|
+
const mcpBlocked = blockedSegment(rootCfg, MCP_KEY.writePath);
|
|
188
|
+
if (mcpBlocked !== undefined) {
|
|
189
|
+
blockedReasons.push(`${mcpBlocked} is not an object; ${MCP_KEY.label} is not written — fix the conflicting key manually`);
|
|
72
190
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
throw new UserFacingError(`${file} is not valid JSONC: ${err instanceof Error ? err.message : err}`);
|
|
76
|
-
}
|
|
77
|
-
return asConfig(parsed, file);
|
|
78
|
-
}
|
|
79
|
-
async writeConfig(projectRoot, cmd) {
|
|
80
|
-
const file = this.configFile(projectRoot);
|
|
81
|
-
const cfg = (await this.readConfig(projectRoot)) ?? {};
|
|
82
|
-
const mcp = asRecord(cfg.mcp) ?? {};
|
|
83
|
-
// каноническая проекция McpCommand в формат opencode: command — массив
|
|
84
|
-
const desired = { type: 'local', command: [cmd.command, ...cmd.args], enabled: true };
|
|
85
|
-
const mcpOk = JSON.stringify(mcp.wolf) === JSON.stringify(desired);
|
|
86
|
-
// §6.1: default_agent и subagent_depth мерджатся рядом с mcp.wolf
|
|
87
|
-
const da = cfg.default_agent;
|
|
88
|
-
const sd = cfg.subagent_depth;
|
|
89
|
-
const reasons = [];
|
|
191
|
+
// §6.1: default_agent — по эффективному виду (корень + .opencode)
|
|
192
|
+
const da = locatePath(surface.files, DEFAULT_AGENT_KEY.writePath)?.value;
|
|
90
193
|
if (da !== undefined && da !== DEFAULT_AGENT) {
|
|
91
|
-
|
|
194
|
+
agentReasons.push(`${DEFAULT_AGENT_KEY.label}=${da} is taken; ${DEFAULT_AGENT} not assigned`);
|
|
195
|
+
}
|
|
196
|
+
// 6.7: объявленное в .opencode не перекрываем вторым ключом в корне
|
|
197
|
+
let writeMcp = false;
|
|
198
|
+
let replaceMcp = false;
|
|
199
|
+
let mcpNativeReason;
|
|
200
|
+
if (nativeAt !== undefined) {
|
|
201
|
+
if (JSON.stringify(nativeAt.value) === JSON.stringify(desired)) {
|
|
202
|
+
// уже канонично — писать нечего
|
|
203
|
+
}
|
|
204
|
+
else if (nativeAt.location === WRITE_LOCATION_ID) {
|
|
205
|
+
if (mcpWritable) {
|
|
206
|
+
writeMcp = true;
|
|
207
|
+
replaceMcp = true;
|
|
208
|
+
mcpNativeReason = `replaced non-canonical ${MCP_KEY.label} entry with the canonical Wolf entry`;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
mcpNativeReason = `${MCP_KEY.label} is declared in ${relOf(surface, nativeAt.location)} — the .opencode value overrides the root one, so a root entry would have no effect; wolf leaves it untouched (wolf writes the root config)`;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
else if (legacyAt === undefined || !legacyMcpOurs || legacyAt.location === WRITE_LOCATION_ID) {
|
|
216
|
+
writeMcp = mcpWritable;
|
|
217
|
+
}
|
|
218
|
+
// легаси-форма: снимаем только свою и только в корне (писать можно лишь туда)
|
|
219
|
+
let dropLegacyMcp = false;
|
|
220
|
+
let mcpLegacyReason;
|
|
221
|
+
if (legacyAt !== undefined) {
|
|
222
|
+
if (!legacyMcpOurs) {
|
|
223
|
+
mcpLegacyReason = `top-level ${legacyMcpPath.join('.')} is deprecated and foreign (command differs); ${MCP_KEY.label} is used`;
|
|
224
|
+
}
|
|
225
|
+
else if (legacyAt.location === WRITE_LOCATION_ID) {
|
|
226
|
+
dropLegacyMcp = true;
|
|
227
|
+
mcpLegacyReason = `removed deprecated top-level ${legacyMcpPath.join('.')} (migrated to ${MCP_KEY.label})`;
|
|
228
|
+
}
|
|
229
|
+
else if (nativeAt === undefined) {
|
|
230
|
+
mcpLegacyReason = `wolf entry is in the deprecated top-level ${legacyMcpPath.join('.')} form in ${relOf(surface, legacyAt.location)} — the .opencode value overrides the root one, so a root entry would have no effect; migrate it to ${MCP_KEY.label} manually`;
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
mcpLegacyReason = `deprecated top-level ${legacyMcpPath.join('.')} in ${relOf(surface, legacyAt.location)} is ignored by opencode v2 — the .opencode value overrides the root one; remove it manually`;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// opencode v2: действует только experimental.subagent_depth. Верхнеуровневый ключ —
|
|
237
|
+
// deprecated: наше точное значение мигрируем, чужое не трогаем и сообщаем.
|
|
238
|
+
const depthPath = SUBAGENT_DEPTH_KEY.writePath;
|
|
239
|
+
const depthLeaf = leaf(SUBAGENT_DEPTH_KEY);
|
|
240
|
+
const depthLabel = dotted(SUBAGENT_DEPTH_KEY);
|
|
241
|
+
const legacyDepthPath = legacyPathOf(SUBAGENT_DEPTH_KEY);
|
|
242
|
+
const depthAt = locatePath(surface.files, depthPath);
|
|
243
|
+
const legacyDepthAt = locatePath(surface.files, legacyDepthPath);
|
|
244
|
+
const depth = depthAt?.value;
|
|
245
|
+
const legacyDepth = legacyDepthAt?.value;
|
|
246
|
+
const depthOk = depthIsOurs(depth);
|
|
247
|
+
const legacyOurs = legacyDepth === SUBAGENT_DEPTH;
|
|
248
|
+
let setDepth = false;
|
|
249
|
+
let dropLegacy = false;
|
|
250
|
+
const depthWritable = pathWritable(rootCfg, depthPath);
|
|
251
|
+
if (!depthWritable) {
|
|
252
|
+
depthReasons.push(`experimental is not an object; ${depthLeaf} is not written — set ${depthLabel}: ${SUBAGENT_DEPTH} manually`);
|
|
253
|
+
}
|
|
254
|
+
else if (depthOk) {
|
|
255
|
+
if (legacyOurs) {
|
|
256
|
+
if (legacyDepthAt.location === WRITE_LOCATION_ID) {
|
|
257
|
+
dropLegacy = true; // оба ключа наши: легаси — только шум, убираем
|
|
258
|
+
depthReasons.push(`removed deprecated top-level ${depthLeaf}=${SUBAGENT_DEPTH} (ignored by opencode v2)`);
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
depthReasons.push(`top-level ${depthLeaf}=${SUBAGENT_DEPTH} in ${relOf(surface, legacyDepthAt.location)} is deprecated and ignored by opencode v2 — the .opencode value overrides the root one; remove it manually`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
else if (legacyDepth !== undefined) {
|
|
265
|
+
depthReasons.push(`top-level ${depthLeaf}=${legacyDepth} is deprecated and ignored by opencode v2; ${depthLabel} is used`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
else if (depth !== undefined) {
|
|
269
|
+
depthReasons.push(`${depthLabel}=${depth} is taken; the three-level scheme will not work, set >=2`);
|
|
270
|
+
}
|
|
271
|
+
else if (legacyOurs) {
|
|
272
|
+
setDepth = true;
|
|
273
|
+
if (legacyDepthAt.location === WRITE_LOCATION_ID) {
|
|
274
|
+
dropLegacy = true;
|
|
275
|
+
depthReasons.push(`migrated ${depthLeaf} to ${depthLabel}`);
|
|
276
|
+
}
|
|
277
|
+
else {
|
|
278
|
+
// легаси в .opencode удалить не можем (wolf пишет только в корень) — но и второй
|
|
279
|
+
// ключ не нужен: каноническую форму wolf записывает в корневой конфиг сам.
|
|
280
|
+
depthReasons.push(`top-level ${depthLeaf}=${SUBAGENT_DEPTH} in ${relOf(surface, legacyDepthAt.location)} is deprecated and ignored by opencode v2 — wolf wrote ${depthLabel}=${SUBAGENT_DEPTH} in the root config; remove the deprecated key manually`);
|
|
281
|
+
}
|
|
92
282
|
}
|
|
93
|
-
if (
|
|
94
|
-
|
|
283
|
+
else if (legacyDepth !== undefined) {
|
|
284
|
+
depthReasons.push(`top-level ${depthLeaf}=${legacyDepth} is taken; the key is deprecated and ignored by opencode v2 — set ${depthLabel} >=${SUBAGENT_DEPTH}`);
|
|
95
285
|
}
|
|
286
|
+
else {
|
|
287
|
+
setDepth = true;
|
|
288
|
+
}
|
|
289
|
+
const reasons = [
|
|
290
|
+
...blockedReasons,
|
|
291
|
+
...agentReasons,
|
|
292
|
+
...(mcpLegacyReason !== undefined ? [mcpLegacyReason] : []),
|
|
293
|
+
...(mcpNativeReason !== undefined ? [mcpNativeReason] : []),
|
|
294
|
+
...depthReasons,
|
|
295
|
+
// битая/нечитаемая локация не мешает починке корня — но называем её
|
|
296
|
+
...surface.errors.map((error) => `skipped: ${error.rel} is ${error.kind}`),
|
|
297
|
+
];
|
|
96
298
|
const reason = reasons.length > 0 ? reasons.join('; ') : undefined;
|
|
97
|
-
// F6 (спека 2.1.0 §2.4): фактические wolf-ключи после ensured —
|
|
98
|
-
// (mcp.wolf
|
|
299
|
+
// F6 (спека 2.1.0 §2.4): фактические wolf-ключи после ensured — по эффективному виду
|
|
300
|
+
// (mcp.servers.wolf каноничен; default_agent/subagent_depth — если ключ наш, не занят чужим значением)
|
|
99
301
|
const ours = () => {
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
302
|
+
const effective = effectiveAfter(surface, rootCfg);
|
|
303
|
+
const keys = [];
|
|
304
|
+
if (isWolfMcpEntry(getPath(effective, MCP_KEY.writePath), cmd))
|
|
305
|
+
keys.push(MCP_KEY.label);
|
|
306
|
+
if (getPath(effective, DEFAULT_AGENT_KEY.writePath) === DEFAULT_AGENT)
|
|
307
|
+
keys.push(`${DEFAULT_AGENT_KEY.label}=${DEFAULT_AGENT}`);
|
|
308
|
+
const sd = getPath(effective, depthPath);
|
|
309
|
+
if (depthIsOurs(sd))
|
|
310
|
+
keys.push(`${depthLeaf}=${sd}`);
|
|
105
311
|
return keys;
|
|
106
312
|
};
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
313
|
+
// F-10: отчёт init называет файл, где реально лежат wolf-ключи (при отсутствии
|
|
314
|
+
// записи — файл, где объявлен эффективный wolf-ключ; иначе — файл записи).
|
|
315
|
+
const wolfKeyFile = () => {
|
|
316
|
+
const mcp = locatePath(surface.files, MCP_KEY.writePath);
|
|
317
|
+
if (mcp !== undefined)
|
|
318
|
+
return relOf(surface, mcp.location);
|
|
319
|
+
const agent = locatePath(surface.files, DEFAULT_AGENT_KEY.writePath);
|
|
320
|
+
if (agent !== undefined)
|
|
321
|
+
return relOf(surface, agent.location);
|
|
322
|
+
const depth = locatePath(surface.files, SUBAGENT_DEPTH_KEY.writePath);
|
|
323
|
+
if (depth !== undefined)
|
|
324
|
+
return relOf(surface, depth.location);
|
|
325
|
+
return undefined;
|
|
326
|
+
};
|
|
327
|
+
// unchanged — по ВСЕМ ключам: корректный mcp.servers.wolf не маскирует отсутствие default_agent/subagent_depth;
|
|
328
|
+
// занятое чужим значением место depth тоже settled (не перезаписываем — только reason);
|
|
329
|
+
// наш легаси mcp.wolf — не settled: его надо снять записью.
|
|
330
|
+
const setAgent = da === undefined;
|
|
331
|
+
if (!writeMcp && !dropLegacyMcp && !setAgent && !setDepth && !dropLegacy)
|
|
332
|
+
return { action: 'unchanged', reason, configFile: wolfKeyFile() ?? basename(file), keys: ours() };
|
|
333
|
+
if (writeMcp)
|
|
334
|
+
setPath(rootCfg, MCP_KEY.writePath, desired);
|
|
335
|
+
if (dropLegacyMcp)
|
|
336
|
+
deletePath(rootCfg, legacyMcpPath);
|
|
337
|
+
if (setAgent)
|
|
338
|
+
setPath(rootCfg, DEFAULT_AGENT_KEY.writePath, DEFAULT_AGENT); // конфликтный ключ не трогаем
|
|
339
|
+
if (setDepth)
|
|
340
|
+
setPath(rootCfg, depthPath, SUBAGENT_DEPTH);
|
|
341
|
+
if (dropLegacy)
|
|
342
|
+
deletePath(rootCfg, legacyDepthPath);
|
|
343
|
+
await writeFileAtomic(file, JSON.stringify(rootCfg, null, 2) + '\n');
|
|
344
|
+
return { action: replaceMcp ? 'replaced' : 'written', reason, configFile: basename(file), keys: ours() };
|
|
119
345
|
}
|
|
346
|
+
/**
|
|
347
|
+
* 6.7: wolf-запись MCP (нативная и легаси, только если она наша) удаляется
|
|
348
|
+
* из ВСЕХ локаций, где она реально объявлена.
|
|
349
|
+
*/
|
|
120
350
|
async removeWolf(projectRoot) {
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
if (cfg === null)
|
|
351
|
+
const surface = await this.loadSurface(projectRoot);
|
|
352
|
+
if (surface.files.length === 0)
|
|
124
353
|
return false;
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
354
|
+
const legacyMcpPath = legacyPathOf(MCP_KEY);
|
|
355
|
+
let removed = false;
|
|
356
|
+
for (const file of surface.files) {
|
|
357
|
+
let changed = false;
|
|
358
|
+
const nativeEntry = getPath(file.cfg, MCP_KEY.writePath);
|
|
359
|
+
const legacyEntry = getPath(file.cfg, legacyMcpPath);
|
|
360
|
+
if (nativeEntry !== undefined && isWolfMcpEntry(nativeEntry, WOLF_CMD)) {
|
|
361
|
+
deletePath(file.cfg, MCP_KEY.writePath);
|
|
362
|
+
changed = true;
|
|
363
|
+
}
|
|
364
|
+
if (legacyEntry !== undefined && isWolfMcpEntry(legacyEntry, WOLF_CMD)) {
|
|
365
|
+
deletePath(file.cfg, legacyMcpPath);
|
|
366
|
+
changed = true;
|
|
367
|
+
}
|
|
368
|
+
if (!changed)
|
|
369
|
+
continue;
|
|
370
|
+
removed = true;
|
|
371
|
+
await writeFileAtomic(file.file, JSON.stringify(file.cfg, null, 2) + '\n');
|
|
372
|
+
}
|
|
373
|
+
return removed;
|
|
135
374
|
}
|
|
136
375
|
/**
|
|
137
376
|
* 2.16.2 (uninstall, F-07): только wolf-ключи с точным нашим значением.
|
|
138
377
|
* Пользовательские default_agent/subagent_depth (не наши точные значения) не трогаем.
|
|
378
|
+
* Глубина: `experimental.subagent_depth === 2` (действующий путь) + легаси верхнеуровневый.
|
|
379
|
+
* 6.3: удаляем обе формы MCP (нативную и легаси) — только если запись наша.
|
|
380
|
+
* 6.7: снимаем ключи из ВСЕХ локаций, где они объявлены; битый файл — в причину.
|
|
139
381
|
*/
|
|
140
382
|
async removeWolfKeys(projectRoot) {
|
|
141
|
-
const file = this.
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
else
|
|
152
|
-
cfg.mcp = mcp;
|
|
153
|
-
removed.push('mcp.wolf');
|
|
154
|
-
}
|
|
155
|
-
if (cfg.default_agent === DEFAULT_AGENT) {
|
|
156
|
-
delete cfg.default_agent;
|
|
157
|
-
removed.push(`default_agent=${DEFAULT_AGENT}`);
|
|
158
|
-
}
|
|
159
|
-
if (cfg.subagent_depth === SUBAGENT_DEPTH) {
|
|
160
|
-
delete cfg.subagent_depth;
|
|
161
|
-
removed.push(`subagent_depth=${SUBAGENT_DEPTH}`);
|
|
383
|
+
const file = this.rootFile(projectRoot);
|
|
384
|
+
const surface = await this.loadSurface(projectRoot);
|
|
385
|
+
const broken = surface.errors.map((error) => `skipped: ${error.rel} is ${error.kind}`);
|
|
386
|
+
if (surface.files.length === 0) {
|
|
387
|
+
// «no opencode.json» — только когда конфиг-файлов нет НИ В ОДНОЙ локации
|
|
388
|
+
return {
|
|
389
|
+
action: 'skipped',
|
|
390
|
+
reason: broken.length > 0 ? broken.join('; ') : `no ${basename(file)}`,
|
|
391
|
+
configFile: basename(file),
|
|
392
|
+
};
|
|
162
393
|
}
|
|
394
|
+
let mcpRemoved = false;
|
|
395
|
+
let agentRemoved = false;
|
|
396
|
+
let depthRemoved = false;
|
|
397
|
+
const changed = [];
|
|
398
|
+
const legacyMcpPath = legacyPathOf(MCP_KEY);
|
|
399
|
+
const depthLeaf = leaf(SUBAGENT_DEPTH_KEY);
|
|
400
|
+
const legacyDepthPath = legacyPathOf(SUBAGENT_DEPTH_KEY);
|
|
401
|
+
// корень первым — так же, как в configFile списке результата
|
|
402
|
+
for (const file of [...surface.files].reverse()) {
|
|
403
|
+
let touched = false;
|
|
404
|
+
const nativeEntry = getPath(file.cfg, MCP_KEY.writePath);
|
|
405
|
+
const legacyEntry = getPath(file.cfg, legacyMcpPath);
|
|
406
|
+
if (nativeEntry !== undefined && isWolfMcpEntry(nativeEntry, WOLF_CMD)) {
|
|
407
|
+
deletePath(file.cfg, MCP_KEY.writePath);
|
|
408
|
+
mcpRemoved = true;
|
|
409
|
+
touched = true;
|
|
410
|
+
}
|
|
411
|
+
if (legacyEntry !== undefined && isWolfMcpEntry(legacyEntry, WOLF_CMD)) {
|
|
412
|
+
deletePath(file.cfg, legacyMcpPath);
|
|
413
|
+
mcpRemoved = true;
|
|
414
|
+
touched = true;
|
|
415
|
+
}
|
|
416
|
+
if (getPath(file.cfg, DEFAULT_AGENT_KEY.writePath) === DEFAULT_AGENT) {
|
|
417
|
+
deletePath(file.cfg, DEFAULT_AGENT_KEY.writePath);
|
|
418
|
+
agentRemoved = true;
|
|
419
|
+
touched = true;
|
|
420
|
+
}
|
|
421
|
+
if (getPath(file.cfg, SUBAGENT_DEPTH_KEY.writePath) === SUBAGENT_DEPTH) {
|
|
422
|
+
deletePath(file.cfg, SUBAGENT_DEPTH_KEY.writePath);
|
|
423
|
+
depthRemoved = true;
|
|
424
|
+
touched = true;
|
|
425
|
+
}
|
|
426
|
+
if (getPath(file.cfg, legacyDepthPath) === SUBAGENT_DEPTH) {
|
|
427
|
+
// легаси верхнеуровневый ключ (deprecated в opencode v2) — тоже наш
|
|
428
|
+
deletePath(file.cfg, legacyDepthPath);
|
|
429
|
+
depthRemoved = true;
|
|
430
|
+
touched = true;
|
|
431
|
+
}
|
|
432
|
+
if (!touched)
|
|
433
|
+
continue; // перезаписываем только файлы, из которых сняты наши ключи
|
|
434
|
+
changed.push(file.rel);
|
|
435
|
+
await writeFileAtomic(file.file, JSON.stringify(file.cfg, null, 2) + '\n');
|
|
436
|
+
}
|
|
437
|
+
const removed = [
|
|
438
|
+
...(mcpRemoved ? [MCP_KEY.label] : []),
|
|
439
|
+
...(agentRemoved ? [`${DEFAULT_AGENT_KEY.label}=${DEFAULT_AGENT}`] : []),
|
|
440
|
+
...(depthRemoved ? [`${depthLeaf}=${SUBAGENT_DEPTH}`] : []),
|
|
441
|
+
];
|
|
163
442
|
if (removed.length === 0) {
|
|
164
443
|
return {
|
|
165
444
|
action: 'unchanged',
|
|
166
|
-
reason: 'no wolf keys with our exact values',
|
|
445
|
+
reason: ['no wolf keys with our exact values', ...broken].join('; '),
|
|
167
446
|
configFile: basename(file),
|
|
168
447
|
keys: [],
|
|
169
448
|
};
|
|
170
449
|
}
|
|
171
|
-
|
|
172
|
-
|
|
450
|
+
return {
|
|
451
|
+
action: 'removed',
|
|
452
|
+
...(broken.length > 0 ? { reason: broken.join('; ') } : {}),
|
|
453
|
+
configFile: changed.join(', '),
|
|
454
|
+
keys: removed,
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* 6.1/6.3/6.4: doctor-проверки, которые знает только opencode-адаптер —
|
|
459
|
+
* все ключи, которые Wolf пишет, + смешанный frontmatter managed-агентов.
|
|
460
|
+
* Строки идут в общий `issues` проекта и маскируются doctor'ом как обычно.
|
|
461
|
+
* 6.7: проверки — по эффективному виду обеих локаций; битый файл — свой issue с именем.
|
|
462
|
+
*/
|
|
463
|
+
async configIssues(projectRoot) {
|
|
464
|
+
const surface = await this.loadSurface(projectRoot);
|
|
465
|
+
const issues = surface.errors.map((error) => `${this.id}: ${error.rel} is ${error.kind} — fix it or remove the file, then run wolf init`);
|
|
466
|
+
if (surface.files.length === 0) {
|
|
467
|
+
// «missing» советовал бы init, который честно скипает битый конфиг
|
|
468
|
+
if (issues.length === 0)
|
|
469
|
+
return [`${this.id}: wolf entry missing — run wolf init`];
|
|
470
|
+
return issues;
|
|
471
|
+
}
|
|
472
|
+
return [
|
|
473
|
+
...issues,
|
|
474
|
+
...mcpIssues(surface),
|
|
475
|
+
...defaultAgentIssues(surface.effective),
|
|
476
|
+
...depthIssues(surface),
|
|
477
|
+
...(await mixedFrontmatterIssues(projectRoot, this.id)),
|
|
478
|
+
];
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
/** Собственный конфиг корневой локации (не слейдженный): в него wolf пишет. */
|
|
482
|
+
function rootConfigOf(surface) {
|
|
483
|
+
return surface.files.find((f) => f.location === WRITE_LOCATION_ID)?.cfg ?? {};
|
|
484
|
+
}
|
|
485
|
+
/** Путь файла локации относительно корня проекта (для сообщений). */
|
|
486
|
+
function relOf(surface, location) {
|
|
487
|
+
return surface.files.find((f) => f.location === location)?.rel ?? '';
|
|
488
|
+
}
|
|
489
|
+
/** Эффективный вид после записи: обновлённый корень + .opencode (порядок — в mergeSurface). */
|
|
490
|
+
function effectiveAfter(surface, rootCfg) {
|
|
491
|
+
return mergeSurface(surface.files, { location: WRITE_LOCATION_ID, cfg: rootCfg });
|
|
492
|
+
}
|
|
493
|
+
/** 6.3/6.7: wolf-сервер отсутствует в обеих формах / легаси-форма подлежит миграции. */
|
|
494
|
+
function mcpIssues(surface) {
|
|
495
|
+
const cfg = surface.effective;
|
|
496
|
+
// запись невозможна: init не вылечит такой конфиг — не советуем его
|
|
497
|
+
const blocked = blockedSegment(cfg, MCP_KEY.writePath);
|
|
498
|
+
if (blocked !== undefined)
|
|
499
|
+
return [`opencode: ${blocked} is not an object — ${MCP_KEY.label} is not written; fix it manually`];
|
|
500
|
+
const legacyPath = legacyPathOf(MCP_KEY);
|
|
501
|
+
const nativeAt = locatePath(surface.files, MCP_KEY.writePath);
|
|
502
|
+
const legacyAt = locatePath(surface.files, legacyPath);
|
|
503
|
+
// сервер не объявлен НИ В ОДНОЙ локации — только тогда «wolf entry missing»
|
|
504
|
+
if (nativeAt === undefined && legacyAt === undefined)
|
|
505
|
+
return [`opencode: wolf entry missing — run wolf init`];
|
|
506
|
+
if (legacyAt === undefined)
|
|
507
|
+
return [];
|
|
508
|
+
const ours = isWolfMcpEntry(legacyAt.value, WOLF_CMD);
|
|
509
|
+
// легаси в .opencode: init туда не пишет — советуем ручную миграцию с именем файла
|
|
510
|
+
if (legacyAt.location === WRITE_LOCATION_ID) {
|
|
511
|
+
return [
|
|
512
|
+
ours
|
|
513
|
+
? `opencode: wolf entry is in the deprecated top-level ${legacyPath.join('.')} form — run wolf init to migrate it to ${MCP_KEY.label}`
|
|
514
|
+
: `opencode: top-level ${legacyPath.join('.')} is foreign and deprecated (opencode v2 reads ${MCP_ROOT}.${MCP_SERVERS}) — wolf leaves it untouched`,
|
|
515
|
+
];
|
|
516
|
+
}
|
|
517
|
+
const rel = relOf(surface, legacyAt.location);
|
|
518
|
+
return [
|
|
519
|
+
ours
|
|
520
|
+
? `opencode: wolf entry is in the deprecated top-level ${legacyPath.join('.')} form in ${rel} — migrate it to ${MCP_KEY.label} manually`
|
|
521
|
+
: `opencode: top-level ${legacyPath.join('.')} in ${rel} is foreign and deprecated (opencode v2 reads ${MCP_ROOT}.${MCP_SERVERS}) — wolf leaves it untouched`,
|
|
522
|
+
];
|
|
523
|
+
}
|
|
524
|
+
/** 6.1: default_agent === mr-wolf, иначе L0 стартует не тем агентом. */
|
|
525
|
+
function defaultAgentIssues(cfg) {
|
|
526
|
+
const da = getPath(cfg, DEFAULT_AGENT_KEY.writePath);
|
|
527
|
+
if (da === undefined)
|
|
528
|
+
return [
|
|
529
|
+
`opencode: ${DEFAULT_AGENT_KEY.label} is not set — ${DEFAULT_AGENT} is not the default agent; run wolf init`,
|
|
530
|
+
];
|
|
531
|
+
if (da !== DEFAULT_AGENT)
|
|
532
|
+
return [
|
|
533
|
+
`opencode: ${DEFAULT_AGENT_KEY.label}=${String(da)} is taken; ${DEFAULT_AGENT} is not assigned (foreign value is never overwritten)`,
|
|
534
|
+
];
|
|
535
|
+
return [];
|
|
536
|
+
}
|
|
537
|
+
/** 6.1/6.7: experimental.subagent_depth >= 2 + deprecated верхнеуровневый ключ (по эффективному виду). */
|
|
538
|
+
function depthIssues(surface) {
|
|
539
|
+
const cfg = surface.effective;
|
|
540
|
+
const out = [];
|
|
541
|
+
const depthPath = SUBAGENT_DEPTH_KEY.writePath;
|
|
542
|
+
const parentPath = depthPath.slice(0, -1);
|
|
543
|
+
const depthLabel = depthPath.join('.');
|
|
544
|
+
const depthLeaf = depthPath[depthPath.length - 1];
|
|
545
|
+
const parentRaw = getPath(cfg, parentPath);
|
|
546
|
+
const experimental = getRecordAt(cfg, parentPath);
|
|
547
|
+
const depth = experimental?.[depthLeaf];
|
|
548
|
+
if (parentRaw !== undefined && experimental === undefined) {
|
|
549
|
+
out.push(`opencode: ${parentPath.join('.')} is not an object — ${depthLabel} is not set; set it to ${SUBAGENT_DEPTH} manually`);
|
|
550
|
+
}
|
|
551
|
+
else if (depth === undefined) {
|
|
552
|
+
out.push(`opencode: ${depthLabel} is not set — the three-level scheme needs >=${SUBAGENT_DEPTH}; run wolf init`);
|
|
553
|
+
}
|
|
554
|
+
else if (typeof depth !== 'number' || depth < SUBAGENT_DEPTH) {
|
|
555
|
+
out.push(`opencode: ${depthLabel}=${String(depth)} — the three-level scheme needs >=${SUBAGENT_DEPTH}; foreign value is never overwritten`);
|
|
556
|
+
}
|
|
557
|
+
const legacyPath = legacyPathOf(SUBAGENT_DEPTH_KEY);
|
|
558
|
+
const legacyAt = locatePath(surface.files, legacyPath);
|
|
559
|
+
if (legacyAt !== undefined) {
|
|
560
|
+
const legacy = legacyAt.value;
|
|
561
|
+
// легаси в .opencode: init туда не пишет — миграция ручная, с именем файла
|
|
562
|
+
if (legacyAt.location === WRITE_LOCATION_ID) {
|
|
563
|
+
out.push(legacy === SUBAGENT_DEPTH
|
|
564
|
+
? `opencode: top-level ${legacyPath.join('.')}=${SUBAGENT_DEPTH} is deprecated and ignored by opencode v2 — run wolf init to migrate it to ${depthLabel}`
|
|
565
|
+
: `opencode: top-level ${legacyPath.join('.')}=${String(legacy)} is deprecated and ignored by opencode v2 — foreign value, set ${depthLabel} >=${SUBAGENT_DEPTH} manually`);
|
|
566
|
+
}
|
|
567
|
+
else {
|
|
568
|
+
const rel = relOf(surface, legacyAt.location);
|
|
569
|
+
out.push(legacy === SUBAGENT_DEPTH
|
|
570
|
+
? `opencode: top-level ${legacyPath.join('.')}=${SUBAGENT_DEPTH} in ${rel} is deprecated and ignored by opencode v2 — migrate it to ${depthLabel} manually`
|
|
571
|
+
: `opencode: top-level ${legacyPath.join('.')}=${String(legacy)} in ${rel} is deprecated and ignored by opencode v2 — foreign value, set ${depthLabel} >=${SUBAGENT_DEPTH} manually`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
return out;
|
|
575
|
+
}
|
|
576
|
+
/** v2-поля агента: их появление включает чтение файла по схеме V2. */
|
|
577
|
+
const V2_AGENT_FIELDS = ['permissions', 'request', 'system', 'disabled'];
|
|
578
|
+
/** v1-поля агента: в V2-режиме молча игнорируются (6.4). */
|
|
579
|
+
const V1_AGENT_FIELDS = ['permission', 'temperature', 'mode'];
|
|
580
|
+
/**
|
|
581
|
+
* 6.4: managed-агент (есть штамп Wolf) со смешанным frontmatter читается как V2,
|
|
582
|
+
* и v1-правила (`permission.task: deny`) исчезают — предупреждаем, не правим файл.
|
|
583
|
+
*/
|
|
584
|
+
async function mixedFrontmatterIssues(projectRoot, platformId) {
|
|
585
|
+
const dir = join(projectRoot, '.opencode', 'agents');
|
|
586
|
+
const names = await fs.readdir(dir).catch(() => []);
|
|
587
|
+
const out = [];
|
|
588
|
+
for (const name of names) {
|
|
589
|
+
if (!name.endsWith('.md'))
|
|
590
|
+
continue;
|
|
591
|
+
const raw = await fs.readFile(join(dir, name), 'utf-8').catch(() => null);
|
|
592
|
+
if (raw === null || parseStamp(raw) === null)
|
|
593
|
+
continue; // чужой файл — не наш scope
|
|
594
|
+
const front = frontmatterOf(raw);
|
|
595
|
+
if (front === undefined)
|
|
596
|
+
continue;
|
|
597
|
+
const v2 = V2_AGENT_FIELDS.filter((field) => front[field] !== undefined);
|
|
598
|
+
const v1 = V1_AGENT_FIELDS.filter((field) => front[field] !== undefined);
|
|
599
|
+
if (v2.length === 0 || v1.length === 0)
|
|
600
|
+
continue;
|
|
601
|
+
out.push(`${platformId}: ${join('.opencode', 'agents', name)} mixes v2 (${v2.join(', ')}) and v1 (${v1.join(', ')}) frontmatter — the file is read as V2 and v1 fields are ignored; do not mix formats in one file`);
|
|
602
|
+
}
|
|
603
|
+
return out;
|
|
604
|
+
}
|
|
605
|
+
/** YAML-frontmatter файла агента; нет блока/битый YAML — undefined. */
|
|
606
|
+
function frontmatterOf(raw) {
|
|
607
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---/.exec(raw);
|
|
608
|
+
if (match === null)
|
|
609
|
+
return undefined;
|
|
610
|
+
try {
|
|
611
|
+
const parsed = yaml.load(match[1]);
|
|
612
|
+
return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
613
|
+
? parsed
|
|
614
|
+
: undefined;
|
|
615
|
+
}
|
|
616
|
+
catch {
|
|
617
|
+
return undefined;
|
|
173
618
|
}
|
|
174
619
|
}
|
|
175
620
|
function asRecord(value) {
|
|
@@ -177,11 +622,4 @@ function asRecord(value) {
|
|
|
177
622
|
? value
|
|
178
623
|
: undefined;
|
|
179
624
|
}
|
|
180
|
-
function asConfig(value, file) {
|
|
181
|
-
const cfg = asRecord(value);
|
|
182
|
-
if (cfg === undefined) {
|
|
183
|
-
throw new UserFacingError(`${file} is not a JSON object — refusing to touch it`);
|
|
184
|
-
}
|
|
185
|
-
return cfg;
|
|
186
|
-
}
|
|
187
625
|
//# sourceMappingURL=opencode-adapter.js.map
|