@xcanwin/manyoyo 4.1.1 → 4.1.10
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/bin/manyoyo.js +325 -1016
- package/docker/manyoyo.Dockerfile +36 -39
- package/lib/agent-resume.js +72 -0
- package/lib/container-run.js +39 -0
- package/lib/image-build.js +323 -0
- package/lib/init-config.js +401 -0
- package/lib/web/frontend/app.css +420 -190
- package/lib/web/frontend/app.html +71 -4
- package/lib/web/frontend/app.js +840 -136
- package/lib/web/frontend/login.css +77 -63
- package/lib/web/server.js +757 -128
- package/package.json +2 -2
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
|
|
7
|
+
function readJsonFileSafely(filePath, label, ctx) {
|
|
8
|
+
if (!fs.existsSync(filePath)) return null;
|
|
9
|
+
try {
|
|
10
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
11
|
+
} catch (e) {
|
|
12
|
+
const { YELLOW, NC } = ctx.colors;
|
|
13
|
+
ctx.log(`${YELLOW}⚠️ ${label} 解析失败: ${filePath}${NC}`);
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseSimpleToml(content) {
|
|
19
|
+
const result = {};
|
|
20
|
+
let current = result;
|
|
21
|
+
|
|
22
|
+
for (const rawLine of String(content || '').split('\n')) {
|
|
23
|
+
const line = rawLine.trim();
|
|
24
|
+
if (!line || line.startsWith('#')) continue;
|
|
25
|
+
|
|
26
|
+
const sectionMatch = line.match(/^\[([^\]]+)\]$/);
|
|
27
|
+
if (sectionMatch) {
|
|
28
|
+
current = result;
|
|
29
|
+
for (const part of sectionMatch[1].split('.').map(v => v.trim()).filter(Boolean)) {
|
|
30
|
+
if (!current[part] || typeof current[part] !== 'object' || Array.isArray(current[part])) {
|
|
31
|
+
current[part] = {};
|
|
32
|
+
}
|
|
33
|
+
current = current[part];
|
|
34
|
+
}
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const keyValueMatch = line.match(/^([A-Za-z0-9_.-]+)\s*=\s*(.+)$/);
|
|
39
|
+
if (!keyValueMatch) continue;
|
|
40
|
+
|
|
41
|
+
const key = keyValueMatch[1];
|
|
42
|
+
let valueText = keyValueMatch[2].trim();
|
|
43
|
+
if ((valueText.startsWith('"') && valueText.endsWith('"')) || (valueText.startsWith("'") && valueText.endsWith("'"))) {
|
|
44
|
+
valueText = valueText.slice(1, -1);
|
|
45
|
+
} else if (valueText === 'true') {
|
|
46
|
+
valueText = true;
|
|
47
|
+
} else if (valueText === 'false') {
|
|
48
|
+
valueText = false;
|
|
49
|
+
} else if (/^-?\d+(\.\d+)?$/.test(valueText)) {
|
|
50
|
+
valueText = Number(valueText);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
current[key] = valueText;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readTomlFileSafely(filePath, label, ctx) {
|
|
60
|
+
if (!fs.existsSync(filePath)) return null;
|
|
61
|
+
try {
|
|
62
|
+
return parseSimpleToml(fs.readFileSync(filePath, 'utf-8'));
|
|
63
|
+
} catch (e) {
|
|
64
|
+
const { YELLOW, NC } = ctx.colors;
|
|
65
|
+
ctx.log(`${YELLOW}⚠️ ${label} 解析失败: ${filePath}${NC}`);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function dedupeList(list) {
|
|
71
|
+
return Array.from(new Set((list || []).filter(Boolean)));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function setInitValue(values, key, value) {
|
|
75
|
+
if (value === undefined || value === null) return;
|
|
76
|
+
const text = String(value).replace(/[\r\n\0]/g, '').trim();
|
|
77
|
+
if (!text) return;
|
|
78
|
+
values[key] = text;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function fillValuesFromEnv(keys, values) {
|
|
82
|
+
keys.forEach(key => setInitValue(values, key, process.env[key]));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function isSafeInitEnvValue(value) {
|
|
86
|
+
if (value === undefined || value === null) return false;
|
|
87
|
+
const text = String(value).replace(/[\r\n\0]/g, '').trim();
|
|
88
|
+
if (!text) return false;
|
|
89
|
+
if (/[\$\(\)\`\|\&\*\{\};<>]/.test(text)) return false;
|
|
90
|
+
if (/^\(/.test(text)) return false;
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function resolveEnvPlaceholder(value) {
|
|
95
|
+
if (typeof value !== 'string') return '';
|
|
96
|
+
const match = value.match(/\{env:([A-Za-z_][A-Za-z0-9_]*)\}/);
|
|
97
|
+
if (!match) return '';
|
|
98
|
+
const envName = match[1];
|
|
99
|
+
return process.env[envName] ? String(process.env[envName]).trim() : '';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function normalizeInitConfigAgents(rawAgents, ctx) {
|
|
103
|
+
const aliasMap = {
|
|
104
|
+
all: 'all',
|
|
105
|
+
claude: 'claude',
|
|
106
|
+
c: 'claude',
|
|
107
|
+
cc: 'claude',
|
|
108
|
+
codex: 'codex',
|
|
109
|
+
cx: 'codex',
|
|
110
|
+
gemini: 'gemini',
|
|
111
|
+
gm: 'gemini',
|
|
112
|
+
g: 'gemini',
|
|
113
|
+
opencode: 'opencode',
|
|
114
|
+
oc: 'opencode'
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
if (rawAgents === true || rawAgents === undefined || rawAgents === null || rawAgents === '') {
|
|
118
|
+
return [...ctx.supportedAgents];
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const tokens = String(rawAgents).split(/[,\s]+/).map(v => v.trim().toLowerCase()).filter(Boolean);
|
|
122
|
+
if (tokens.length === 0) return [...ctx.supportedAgents];
|
|
123
|
+
|
|
124
|
+
const normalized = [];
|
|
125
|
+
for (const token of tokens) {
|
|
126
|
+
const mapped = aliasMap[token];
|
|
127
|
+
if (!mapped) {
|
|
128
|
+
const { RED, YELLOW, NC } = ctx.colors;
|
|
129
|
+
ctx.error(`${RED}⚠️ 错误: --init-config 不支持的 Agent: ${token}${NC}`);
|
|
130
|
+
ctx.error(`${YELLOW}支持: ${ctx.supportedAgents.join(', ')} 或 all${NC}`);
|
|
131
|
+
ctx.exit(1);
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
if (mapped === 'all') return [...ctx.supportedAgents];
|
|
135
|
+
if (!normalized.includes(mapped)) normalized.push(mapped);
|
|
136
|
+
}
|
|
137
|
+
return normalized;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function collectClaudeInitData(homeDir, ctx) {
|
|
141
|
+
const keys = [
|
|
142
|
+
'ANTHROPIC_AUTH_TOKEN',
|
|
143
|
+
'CLAUDE_CODE_OAUTH_TOKEN',
|
|
144
|
+
'ANTHROPIC_BASE_URL',
|
|
145
|
+
'ANTHROPIC_MODEL',
|
|
146
|
+
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
|
147
|
+
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
148
|
+
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
|
|
149
|
+
'CLAUDE_CODE_SUBAGENT_MODEL'
|
|
150
|
+
];
|
|
151
|
+
const values = {};
|
|
152
|
+
fillValuesFromEnv(keys, values);
|
|
153
|
+
|
|
154
|
+
const settingsJson = readJsonFileSafely(path.join(homeDir, '.claude', 'settings.json'), 'Claude settings', ctx);
|
|
155
|
+
if (settingsJson && settingsJson.env && typeof settingsJson.env === 'object') {
|
|
156
|
+
keys.forEach(key => setInitValue(values, key, settingsJson.env[key]));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return { keys, values, notes: [], volumes: [] };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function collectGeminiInitData(homeDir) {
|
|
163
|
+
const keys = ['GOOGLE_GEMINI_BASE_URL', 'GEMINI_API_KEY', 'GEMINI_MODEL'];
|
|
164
|
+
const values = {};
|
|
165
|
+
const notes = [];
|
|
166
|
+
const volumes = [];
|
|
167
|
+
fillValuesFromEnv(keys, values);
|
|
168
|
+
|
|
169
|
+
const geminiDir = path.join(homeDir, '.gemini');
|
|
170
|
+
if (fs.existsSync(geminiDir)) {
|
|
171
|
+
volumes.push(`${geminiDir}:/root/.gemini`);
|
|
172
|
+
} else {
|
|
173
|
+
notes.push('未检测到 Gemini 本地配置目录(~/.gemini),已生成占位模板。');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return { keys, values, notes, volumes };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function collectCodexInitData(homeDir, ctx) {
|
|
180
|
+
const keys = ['OPENAI_API_KEY', 'OPENAI_BASE_URL', 'OPENAI_MODEL'];
|
|
181
|
+
const values = {};
|
|
182
|
+
const notes = [];
|
|
183
|
+
const volumes = [];
|
|
184
|
+
fillValuesFromEnv(keys, values);
|
|
185
|
+
|
|
186
|
+
const codexDir = path.join(homeDir, '.codex');
|
|
187
|
+
const authJson = readJsonFileSafely(path.join(codexDir, 'auth.json'), 'Codex auth', ctx);
|
|
188
|
+
const configToml = readTomlFileSafely(path.join(codexDir, 'config.toml'), 'Codex TOML', ctx);
|
|
189
|
+
|
|
190
|
+
if (authJson && typeof authJson === 'object') {
|
|
191
|
+
setInitValue(values, 'OPENAI_API_KEY', authJson.OPENAI_API_KEY);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (configToml && typeof configToml === 'object') {
|
|
195
|
+
setInitValue(values, 'OPENAI_MODEL', configToml.model);
|
|
196
|
+
const providers = configToml.model_providers;
|
|
197
|
+
let providerConfig = null;
|
|
198
|
+
if (providers && typeof providers === 'object') {
|
|
199
|
+
const selectedProviderName =
|
|
200
|
+
(typeof configToml.model_provider === 'string' && providers[configToml.model_provider])
|
|
201
|
+
? configToml.model_provider
|
|
202
|
+
: Object.keys(providers)[0];
|
|
203
|
+
providerConfig = selectedProviderName ? providers[selectedProviderName] : null;
|
|
204
|
+
}
|
|
205
|
+
if (providerConfig && typeof providerConfig === 'object') {
|
|
206
|
+
setInitValue(values, 'OPENAI_BASE_URL', providerConfig.base_url);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (fs.existsSync(codexDir)) {
|
|
211
|
+
volumes.push(`${codexDir}:/root/.codex`);
|
|
212
|
+
} else {
|
|
213
|
+
notes.push('未检测到 Codex 本地配置目录(~/.codex),已生成占位模板。');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return { keys, values, notes, volumes };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function collectOpenCodeInitData(homeDir, ctx) {
|
|
220
|
+
const keys = ['OPENAI_API_KEY', 'OPENAI_BASE_URL', 'OPENAI_MODEL'];
|
|
221
|
+
const values = {};
|
|
222
|
+
const notes = [];
|
|
223
|
+
const volumes = [];
|
|
224
|
+
fillValuesFromEnv(keys, values);
|
|
225
|
+
|
|
226
|
+
const opencodePath = path.join(homeDir, '.config', 'opencode', 'opencode.json');
|
|
227
|
+
const opencodeAuthPath = path.join(homeDir, '.local', 'share', 'opencode', 'auth.json');
|
|
228
|
+
const opencodeJson = readJsonFileSafely(opencodePath, 'OpenCode config', ctx);
|
|
229
|
+
|
|
230
|
+
if (opencodeJson && typeof opencodeJson === 'object') {
|
|
231
|
+
const providerList = opencodeJson.provider && typeof opencodeJson.provider === 'object'
|
|
232
|
+
? Object.values(opencodeJson.provider).filter(v => v && typeof v === 'object')
|
|
233
|
+
: [];
|
|
234
|
+
const provider = providerList[0];
|
|
235
|
+
|
|
236
|
+
if (provider) {
|
|
237
|
+
const providerOptions = provider.options && typeof provider.options === 'object' ? provider.options : {};
|
|
238
|
+
setInitValue(values, 'OPENAI_API_KEY', resolveEnvPlaceholder(providerOptions.apiKey) || providerOptions.apiKey);
|
|
239
|
+
setInitValue(values, 'OPENAI_BASE_URL', resolveEnvPlaceholder(providerOptions.baseURL) || providerOptions.baseURL);
|
|
240
|
+
|
|
241
|
+
if (provider.models && typeof provider.models === 'object') {
|
|
242
|
+
const firstModelName = Object.keys(provider.models)[0];
|
|
243
|
+
if (firstModelName) setInitValue(values, 'OPENAI_MODEL', firstModelName);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (typeof opencodeJson.model === 'string') {
|
|
248
|
+
const modelFromEnv = resolveEnvPlaceholder(opencodeJson.model);
|
|
249
|
+
if (modelFromEnv) setInitValue(values, 'OPENAI_MODEL', modelFromEnv);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (fs.existsSync(opencodePath)) {
|
|
254
|
+
volumes.push(`${opencodePath}:/root/.config/opencode/opencode.json`);
|
|
255
|
+
} else {
|
|
256
|
+
notes.push('未检测到 OpenCode 配置文件(~/.config/opencode/opencode.json),已生成占位模板。');
|
|
257
|
+
}
|
|
258
|
+
if (fs.existsSync(opencodeAuthPath)) {
|
|
259
|
+
volumes.push(`${opencodeAuthPath}:/root/.local/share/opencode/auth.json`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return { keys, values, notes, volumes: dedupeList(volumes) };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function buildInitRunEnv(keys, values) {
|
|
266
|
+
const envMap = {};
|
|
267
|
+
const missingKeys = [];
|
|
268
|
+
const unsafeKeys = [];
|
|
269
|
+
|
|
270
|
+
for (const key of keys) {
|
|
271
|
+
const value = values[key];
|
|
272
|
+
if (isSafeInitEnvValue(value)) {
|
|
273
|
+
envMap[key] = String(value).replace(/[\r\n\0]/g, '');
|
|
274
|
+
} else if (value !== undefined && value !== null && String(value).trim() !== '') {
|
|
275
|
+
envMap[key] = '';
|
|
276
|
+
unsafeKeys.push(key);
|
|
277
|
+
} else {
|
|
278
|
+
envMap[key] = '';
|
|
279
|
+
missingKeys.push(key);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return { envMap, missingKeys, unsafeKeys };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function buildInitRunProfile(agent, yolo, volumes, keys, values) {
|
|
287
|
+
const envResult = buildInitRunEnv(keys, values);
|
|
288
|
+
const runProfile = {
|
|
289
|
+
containerName: `my-${agent}-{now}`,
|
|
290
|
+
env: envResult.envMap,
|
|
291
|
+
yolo
|
|
292
|
+
};
|
|
293
|
+
const volumeList = dedupeList(volumes);
|
|
294
|
+
if (volumeList.length > 0) runProfile.volumes = volumeList;
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
runProfile,
|
|
298
|
+
missingKeys: envResult.missingKeys,
|
|
299
|
+
unsafeKeys: envResult.unsafeKeys
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function shouldOverwriteInitRunEntry(runName, exists, ctx) {
|
|
304
|
+
const { YELLOW, NC } = ctx.colors;
|
|
305
|
+
if (!exists) return true;
|
|
306
|
+
|
|
307
|
+
if (ctx.yesMode) {
|
|
308
|
+
ctx.log(`${YELLOW}⚠️ runs.${runName} 已存在,--yes 模式自动覆盖${NC}`);
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const reply = await ctx.askQuestion(`❔ runs.${runName} 已存在,是否覆盖? [y/N]: `);
|
|
313
|
+
const firstChar = String(reply || '').trim().toLowerCase()[0];
|
|
314
|
+
if (firstChar === 'y') return true;
|
|
315
|
+
|
|
316
|
+
ctx.log(`${YELLOW}⏭️ 已保留原配置: runs.${runName}${NC}`);
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function initAgentConfigs(rawAgents, options = {}) {
|
|
321
|
+
const ctx = {
|
|
322
|
+
homeDir: options.homeDir || os.homedir(),
|
|
323
|
+
yesMode: Boolean(options.yesMode),
|
|
324
|
+
askQuestion: options.askQuestion || (async () => ''),
|
|
325
|
+
loadConfig: options.loadConfig || (() => ({})),
|
|
326
|
+
supportedAgents: Array.isArray(options.supportedAgents) && options.supportedAgents.length > 0
|
|
327
|
+
? options.supportedAgents
|
|
328
|
+
: ['claude', 'codex', 'gemini', 'opencode'],
|
|
329
|
+
log: options.log || console.log,
|
|
330
|
+
error: options.error || console.error,
|
|
331
|
+
exit: options.exit || (code => process.exit(code)),
|
|
332
|
+
colors: options.colors || { RED: '', GREEN: '', YELLOW: '', CYAN: '', NC: '' }
|
|
333
|
+
};
|
|
334
|
+
const { RED, GREEN, YELLOW, CYAN, NC } = ctx.colors;
|
|
335
|
+
|
|
336
|
+
const agents = normalizeInitConfigAgents(rawAgents, ctx);
|
|
337
|
+
const manyoyoHome = path.join(ctx.homeDir, '.manyoyo');
|
|
338
|
+
const manyoyoConfigPath = path.join(manyoyoHome, 'manyoyo.json');
|
|
339
|
+
fs.mkdirSync(manyoyoHome, { recursive: true });
|
|
340
|
+
|
|
341
|
+
const manyoyoConfig = ctx.loadConfig();
|
|
342
|
+
let runsMap = {};
|
|
343
|
+
if (manyoyoConfig.runs !== undefined) {
|
|
344
|
+
if (typeof manyoyoConfig.runs !== 'object' || manyoyoConfig.runs === null || Array.isArray(manyoyoConfig.runs)) {
|
|
345
|
+
ctx.error(`${RED}⚠️ 错误: ~/.manyoyo/manyoyo.json 的 runs 必须是对象(map)${NC}`);
|
|
346
|
+
ctx.exit(1);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
runsMap = { ...manyoyoConfig.runs };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const extractors = {
|
|
353
|
+
claude: homeDir => collectClaudeInitData(homeDir, ctx),
|
|
354
|
+
codex: homeDir => collectCodexInitData(homeDir, ctx),
|
|
355
|
+
gemini: collectGeminiInitData,
|
|
356
|
+
opencode: homeDir => collectOpenCodeInitData(homeDir, ctx)
|
|
357
|
+
};
|
|
358
|
+
const yoloMap = { claude: 'c', codex: 'cx', gemini: 'gm', opencode: 'oc' };
|
|
359
|
+
|
|
360
|
+
let hasConfigChanged = false;
|
|
361
|
+
ctx.log(`${CYAN}🧭 正在初始化 MANYOYO 配置: ${agents.join(', ')}${NC}`);
|
|
362
|
+
|
|
363
|
+
for (const agent of agents) {
|
|
364
|
+
const data = extractors[agent](ctx.homeDir);
|
|
365
|
+
const shouldWriteRun = await shouldOverwriteInitRunEntry(agent, Object.prototype.hasOwnProperty.call(runsMap, agent), ctx);
|
|
366
|
+
|
|
367
|
+
let writeResult = { missingKeys: [], unsafeKeys: [] };
|
|
368
|
+
if (shouldWriteRun) {
|
|
369
|
+
const buildResult = buildInitRunProfile(agent, yoloMap[agent], data.volumes, data.keys, data.values);
|
|
370
|
+
runsMap[agent] = buildResult.runProfile;
|
|
371
|
+
writeResult = { missingKeys: buildResult.missingKeys, unsafeKeys: buildResult.unsafeKeys };
|
|
372
|
+
hasConfigChanged = true;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (shouldWriteRun) {
|
|
376
|
+
ctx.log(`${GREEN}✅ [${agent}] 初始化完成${NC}`);
|
|
377
|
+
} else {
|
|
378
|
+
ctx.log(`${YELLOW}⚠️ [${agent}] 已跳过(配置保留)${NC}`);
|
|
379
|
+
}
|
|
380
|
+
ctx.log(` run: ${shouldWriteRun ? '已写入' : '保留'} runs.${agent}`);
|
|
381
|
+
|
|
382
|
+
if (shouldWriteRun && writeResult.missingKeys.length > 0) {
|
|
383
|
+
ctx.log(`${YELLOW}⚠️ [${agent}] 以下变量未找到,请手动填写:${NC} ${writeResult.missingKeys.join(', ')}`);
|
|
384
|
+
}
|
|
385
|
+
if (shouldWriteRun && writeResult.unsafeKeys.length > 0) {
|
|
386
|
+
ctx.log(`${YELLOW}⚠️ [${agent}] 以下变量包含不安全字符,已留空 env 键:${NC} ${writeResult.unsafeKeys.join(', ')}`);
|
|
387
|
+
}
|
|
388
|
+
if (data.notes && data.notes.length > 0) {
|
|
389
|
+
data.notes.forEach(note => ctx.log(`${YELLOW}⚠️ [${agent}] ${note}${NC}`));
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (hasConfigChanged || !fs.existsSync(manyoyoConfigPath)) {
|
|
394
|
+
manyoyoConfig.runs = runsMap;
|
|
395
|
+
fs.writeFileSync(manyoyoConfigPath, `${JSON.stringify(manyoyoConfig, null, 4)}\n`);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
module.exports = {
|
|
400
|
+
initAgentConfigs
|
|
401
|
+
};
|