create-merlin-brain 3.11.0 → 3.12.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/bin/install.cjs +146 -22
- package/bin/runtime-adapters.cjs +396 -0
- package/dist/server/cost/tracker.d.ts +38 -2
- package/dist/server/cost/tracker.d.ts.map +1 -1
- package/dist/server/cost/tracker.js +87 -15
- package/dist/server/cost/tracker.js.map +1 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +74 -30
- package/dist/server/server.js.map +1 -1
- package/dist/server/tools/adaptive.js +1 -1
- package/dist/server/tools/adaptive.js.map +1 -1
- package/dist/server/tools/agents-index.js +3 -3
- package/dist/server/tools/agents-index.js.map +1 -1
- package/dist/server/tools/agents.js +5 -5
- package/dist/server/tools/agents.js.map +1 -1
- package/dist/server/tools/behaviors.js +4 -4
- package/dist/server/tools/behaviors.js.map +1 -1
- package/dist/server/tools/context.js +7 -7
- package/dist/server/tools/context.js.map +1 -1
- package/dist/server/tools/cost.d.ts +3 -1
- package/dist/server/tools/cost.d.ts.map +1 -1
- package/dist/server/tools/cost.js +66 -13
- package/dist/server/tools/cost.js.map +1 -1
- package/dist/server/tools/discoveries.js +6 -6
- package/dist/server/tools/discoveries.js.map +1 -1
- package/dist/server/tools/index.d.ts +4 -0
- package/dist/server/tools/index.d.ts.map +1 -1
- package/dist/server/tools/index.js +4 -0
- package/dist/server/tools/index.js.map +1 -1
- package/dist/server/tools/learning.d.ts +12 -0
- package/dist/server/tools/learning.d.ts.map +1 -0
- package/dist/server/tools/learning.js +269 -0
- package/dist/server/tools/learning.js.map +1 -0
- package/dist/server/tools/project.js +7 -7
- package/dist/server/tools/project.js.map +1 -1
- package/dist/server/tools/promote.d.ts +11 -0
- package/dist/server/tools/promote.d.ts.map +1 -0
- package/dist/server/tools/promote.js +315 -0
- package/dist/server/tools/promote.js.map +1 -0
- package/dist/server/tools/route.d.ts.map +1 -1
- package/dist/server/tools/route.js +65 -24
- package/dist/server/tools/route.js.map +1 -1
- package/dist/server/tools/session-restore.d.ts +18 -0
- package/dist/server/tools/session-restore.d.ts.map +1 -0
- package/dist/server/tools/session-restore.js +154 -0
- package/dist/server/tools/session-restore.js.map +1 -0
- package/dist/server/tools/session-search.d.ts +16 -0
- package/dist/server/tools/session-search.d.ts.map +1 -0
- package/dist/server/tools/session-search.js +240 -0
- package/dist/server/tools/session-search.js.map +1 -0
- package/dist/server/tools/sights-index.js +2 -2
- package/dist/server/tools/sights-index.js.map +1 -1
- package/dist/server/tools/smart-route.d.ts.map +1 -1
- package/dist/server/tools/smart-route.js +4 -5
- package/dist/server/tools/smart-route.js.map +1 -1
- package/dist/server/tools/verification.js +1 -1
- package/dist/server/tools/verification.js.map +1 -1
- package/files/agents/code-organization-supervisor.md +1 -0
- package/files/agents/context-guardian.md +1 -0
- package/files/agents/docs-keeper.md +1 -0
- package/files/agents/dry-refactor.md +1 -0
- package/files/agents/elite-code-refactorer.md +1 -0
- package/files/agents/hardening-guard.md +1 -0
- package/files/agents/implementation-dev.md +1 -0
- package/files/agents/merlin-access-control-reviewer.md +248 -0
- package/files/agents/merlin-codebase-mapper.md +1 -1
- package/files/agents/merlin-dependency-auditor.md +216 -0
- package/files/agents/merlin-executor.md +1 -0
- package/files/agents/merlin-input-validator.md +247 -0
- package/files/agents/merlin-reviewer.md +1 -0
- package/files/agents/merlin-sast-reviewer.md +182 -0
- package/files/agents/merlin-secret-scanner.md +203 -0
- package/files/agents/tests-qa.md +1 -0
- package/files/commands/merlin/execute-phase.md +94 -197
- package/files/commands/merlin/execute-plan.md +116 -180
- package/files/commands/merlin/health.md +385 -0
- package/files/commands/merlin/loop-recipes.md +93 -36
- package/files/commands/merlin/optimize-prompts.md +158 -0
- package/files/commands/merlin/profiles.md +215 -0
- package/files/commands/merlin/promote.md +176 -0
- package/files/commands/merlin/quick.md +229 -0
- package/files/commands/merlin/resume-work.md +27 -1
- package/files/commands/merlin/route.md +43 -1
- package/files/commands/merlin/sandbox.md +359 -0
- package/files/commands/merlin/usage.md +55 -0
- package/files/docker/Dockerfile.merlin +20 -0
- package/files/docker/docker-compose.merlin.yml +23 -0
- package/files/hook-templates/auto-commit.sh +64 -0
- package/files/hook-templates/auto-format.sh +95 -0
- package/files/hook-templates/auto-test.sh +117 -0
- package/files/hook-templates/branch-protection.sh +72 -0
- package/files/hook-templates/changelog-reminder.sh +76 -0
- package/files/hook-templates/complexity-check.sh +112 -0
- package/files/hook-templates/import-audit.sh +83 -0
- package/files/hook-templates/license-header.sh +84 -0
- package/files/hook-templates/pr-description.sh +100 -0
- package/files/hook-templates/todo-tracker.sh +80 -0
- package/files/hooks/check-file-size.sh +17 -4
- package/files/hooks/config-change.sh +44 -16
- package/files/hooks/instructions-loaded.sh +22 -5
- package/files/hooks/notify-desktop.sh +157 -0
- package/files/hooks/notify-webhook.sh +141 -0
- package/files/hooks/pre-edit-sights-check.sh +76 -9
- package/files/hooks/security-scanner.sh +153 -0
- package/files/hooks/session-end-memory-sync.sh +97 -0
- package/files/hooks/session-end.sh +274 -1
- package/files/hooks/session-start.sh +19 -6
- package/files/hooks/smart-approve.sh +270 -0
- package/files/hooks/teammate-idle-verify.sh +87 -12
- package/files/hooks/worktree-create.sh +20 -3
- package/files/hooks/worktree-remove.sh +21 -3
- package/files/merlin/references/plan-format.md +37 -9
- package/files/merlin/sandbox.json +9 -0
- package/files/merlin/security.json +11 -0
- package/files/merlin/templates/ci/docs-update.yml +81 -0
- package/files/merlin/templates/ci/pr-review.yml +50 -0
- package/files/merlin/templates/ci/security-audit.yml +74 -0
- package/files/merlin/templates/config.json +9 -1
- package/files/rules/api-rules.md +30 -0
- package/files/rules/frontend-rules.md +25 -0
- package/files/rules/hooks-rules.md +36 -0
- package/files/rules/mcp-rules.md +30 -0
- package/files/rules/worker-rules.md +29 -0
- package/package.json +1 -1
package/bin/install.cjs
CHANGED
|
@@ -11,6 +11,11 @@
|
|
|
11
11
|
const isServeMode = process.argv.includes('serve');
|
|
12
12
|
const isInjectMode = process.argv.includes('inject-headers');
|
|
13
13
|
|
|
14
|
+
// --runtime flag: 'all' | 'claude' | 'codex' | 'opencode' | 'gemini'
|
|
15
|
+
// Default: 'all' (configure all detected runtimes)
|
|
16
|
+
const runtimeFlagIdx = process.argv.indexOf('--runtime');
|
|
17
|
+
const RUNTIME_FLAG = runtimeFlagIdx !== -1 ? (process.argv[runtimeFlagIdx + 1] || 'all') : 'all';
|
|
18
|
+
|
|
14
19
|
// =============================================================================
|
|
15
20
|
// MODE: inject-headers - Add Merlin reminder to CLAUDE.md files
|
|
16
21
|
// =============================================================================
|
|
@@ -101,12 +106,14 @@ const fs = require('fs');
|
|
|
101
106
|
const path = require('path');
|
|
102
107
|
const os = require('os');
|
|
103
108
|
const readline = require('readline');
|
|
109
|
+
const { detectRuntimes, configureRuntimes } = require('./runtime-adapters.cjs');
|
|
104
110
|
|
|
105
111
|
const CLAUDE_DIR = path.join(os.homedir(), '.claude');
|
|
106
112
|
const MERLIN_DIR = path.join(CLAUDE_DIR, 'merlin');
|
|
107
113
|
const AGENTS_DIR = path.join(CLAUDE_DIR, 'agents');
|
|
108
114
|
const COMMANDS_DIR = path.join(CLAUDE_DIR, 'commands', 'merlin');
|
|
109
115
|
const LOOP_DIR = path.join(CLAUDE_DIR, 'loop');
|
|
116
|
+
const RULES_DIR = path.join(CLAUDE_DIR, 'rules');
|
|
110
117
|
|
|
111
118
|
const colors = {
|
|
112
119
|
reset: '\x1b[0m',
|
|
@@ -838,7 +845,7 @@ async function install() {
|
|
|
838
845
|
}
|
|
839
846
|
|
|
840
847
|
// Step 0: Clean up legacy GSD/ccwiki artifacts
|
|
841
|
-
logStep('0/
|
|
848
|
+
logStep('0/13', 'Cleaning up legacy installations...');
|
|
842
849
|
const cleaned = cleanupLegacy();
|
|
843
850
|
if (cleaned.length > 0) {
|
|
844
851
|
for (const item of cleaned) {
|
|
@@ -849,11 +856,24 @@ async function install() {
|
|
|
849
856
|
}
|
|
850
857
|
|
|
851
858
|
// Step 1: Ensure Claude Code is installed and up to date
|
|
852
|
-
logStep('1/
|
|
859
|
+
logStep('1/13', 'Checking Claude Code...');
|
|
853
860
|
const claudeCheck = ensureClaudeCode();
|
|
854
861
|
|
|
855
|
-
// Step 2:
|
|
856
|
-
logStep('2/
|
|
862
|
+
// Step 2: Detect runtimes
|
|
863
|
+
logStep('2/13', 'Detecting runtimes...');
|
|
864
|
+
const detectedRuntimes = detectRuntimes();
|
|
865
|
+
log(` ${colors.green}✅${colors.reset} Claude Code (primary)`);
|
|
866
|
+
for (const rt of detectedRuntimes) {
|
|
867
|
+
const icon = rt.found ? `${colors.green}✅${colors.reset}` : `${colors.yellow}⬚${colors.reset}`;
|
|
868
|
+
const suffix = rt.found ? '' : ' (not found)';
|
|
869
|
+
log(` ${icon} ${rt.label}${suffix}`);
|
|
870
|
+
}
|
|
871
|
+
if (RUNTIME_FLAG === 'claude') {
|
|
872
|
+
log(` ${colors.yellow}--runtime claude:${colors.reset} other runtimes will not be configured`);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
// Step 3: Install globally for instant startup across all terminals
|
|
876
|
+
logStep('3/13', 'Installing globally (fast startup for all terminals)...');
|
|
857
877
|
try {
|
|
858
878
|
const { execSync } = require('child_process');
|
|
859
879
|
// Check if already installed globally and up-to-date
|
|
@@ -893,16 +913,16 @@ async function install() {
|
|
|
893
913
|
useGlobalBinary = false;
|
|
894
914
|
}
|
|
895
915
|
|
|
896
|
-
// Step
|
|
897
|
-
logStep('
|
|
916
|
+
// Step 4: Create directories
|
|
917
|
+
logStep('4/13', 'Creating directories...');
|
|
898
918
|
ensureDir(CLAUDE_DIR);
|
|
899
919
|
ensureDir(MERLIN_DIR);
|
|
900
920
|
ensureDir(AGENTS_DIR);
|
|
901
921
|
ensureDir(COMMANDS_DIR);
|
|
902
922
|
logSuccess('Directories created');
|
|
903
923
|
|
|
904
|
-
// Step
|
|
905
|
-
logStep('
|
|
924
|
+
// Step 5: Install Merlin core (workflows, references, templates)
|
|
925
|
+
logStep('5/13', 'Installing Merlin workflows...');
|
|
906
926
|
const merlinSrc = path.join(filesDir, 'merlin');
|
|
907
927
|
if (fs.existsSync(merlinSrc)) {
|
|
908
928
|
const count = copyDirRecursive(merlinSrc, MERLIN_DIR);
|
|
@@ -914,8 +934,8 @@ async function install() {
|
|
|
914
934
|
logWarn('Merlin workflows not found in package');
|
|
915
935
|
}
|
|
916
936
|
|
|
917
|
-
// Step
|
|
918
|
-
logStep('
|
|
937
|
+
// Step 6: Install agents (tiered)
|
|
938
|
+
logStep('6/13', 'Installing Merlin agents...');
|
|
919
939
|
const agentsSrc = path.join(filesDir, 'agents');
|
|
920
940
|
if (fs.existsSync(agentsSrc)) {
|
|
921
941
|
// Load agent manifest for tiered display
|
|
@@ -942,8 +962,32 @@ async function install() {
|
|
|
942
962
|
logWarn('Agents not found in package');
|
|
943
963
|
}
|
|
944
964
|
|
|
945
|
-
// Step
|
|
946
|
-
logStep('
|
|
965
|
+
// Step 7: Install path-scoped rules
|
|
966
|
+
logStep('7/13', 'Installing path-scoped rules...');
|
|
967
|
+
const rulesSrc = path.join(filesDir, 'rules');
|
|
968
|
+
if (fs.existsSync(rulesSrc)) {
|
|
969
|
+
ensureDir(RULES_DIR);
|
|
970
|
+
const ruleFiles = fs.readdirSync(rulesSrc).filter(f => f.endsWith('.md'));
|
|
971
|
+
let installedCount = 0;
|
|
972
|
+
let skippedCount = 0;
|
|
973
|
+
for (const ruleFile of ruleFiles) {
|
|
974
|
+
const destPath = path.join(RULES_DIR, ruleFile);
|
|
975
|
+
// Never overwrite existing user rules — they may have been customized
|
|
976
|
+
if (fs.existsSync(destPath)) {
|
|
977
|
+
skippedCount++;
|
|
978
|
+
} else {
|
|
979
|
+
fs.copyFileSync(path.join(rulesSrc, ruleFile), destPath);
|
|
980
|
+
installedCount++;
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
if (installedCount > 0) logSuccess(`Installed ${installedCount} path-scoped rule files`);
|
|
984
|
+
if (skippedCount > 0) logSuccess(`Skipped ${skippedCount} existing rule files (user customizations preserved)`);
|
|
985
|
+
} else {
|
|
986
|
+
logWarn('Rules not found in package');
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// Step 8: Install commands
|
|
990
|
+
logStep('8/13', 'Installing /merlin:* commands...');
|
|
947
991
|
const commandsSrc = path.join(filesDir, 'commands', 'merlin');
|
|
948
992
|
if (fs.existsSync(commandsSrc)) {
|
|
949
993
|
const count = copyDirRecursive(commandsSrc, COMMANDS_DIR);
|
|
@@ -952,8 +996,8 @@ async function install() {
|
|
|
952
996
|
logWarn('Commands not found in package');
|
|
953
997
|
}
|
|
954
998
|
|
|
955
|
-
// Step
|
|
956
|
-
logStep('
|
|
999
|
+
// Step 9: Install CLAUDE.md
|
|
1000
|
+
logStep('9/13', 'Configuring Claude Code...');
|
|
957
1001
|
const claudeMdSrc = path.join(filesDir, 'CLAUDE.md');
|
|
958
1002
|
const claudeMdDest = path.join(CLAUDE_DIR, 'CLAUDE.md');
|
|
959
1003
|
|
|
@@ -964,12 +1008,12 @@ async function install() {
|
|
|
964
1008
|
logSuccess('Installed CLAUDE.md (Merlin instructions)');
|
|
965
1009
|
}
|
|
966
1010
|
|
|
967
|
-
// Step
|
|
1011
|
+
// Step 10: Install Merlin Loop (legacy — kept for backward compatibility)
|
|
968
1012
|
// NOTE: Claude Code now has a native /loop command that replaces these scripts.
|
|
969
1013
|
// Use /merlin:loop-recipes in Claude Code for pre-built loop patterns.
|
|
970
1014
|
// These scripts are still copied so existing users and terminal workflows
|
|
971
1015
|
// (merlin-loop, merlin session) continue to work without interruption.
|
|
972
|
-
logStep('
|
|
1016
|
+
logStep('10/13', 'Installing Merlin Loop (legacy scripts)...');
|
|
973
1017
|
const loopSrc = path.join(filesDir, 'loop');
|
|
974
1018
|
if (fs.existsSync(loopSrc)) {
|
|
975
1019
|
ensureDir(LOOP_DIR);
|
|
@@ -996,12 +1040,13 @@ async function install() {
|
|
|
996
1040
|
logSuccess(`Installed ${count} loop files`);
|
|
997
1041
|
logSuccess(`Added 'merlin-loop' command`);
|
|
998
1042
|
logSuccess(`Added 'merlin session' command (interactive orchestrator)`);
|
|
1043
|
+
logWarn(`Note: Custom loop scripts are deprecated. Use /loop with /merlin:loop-recipes instead.`);
|
|
999
1044
|
} else {
|
|
1000
1045
|
logWarn('Merlin Loop not found in package');
|
|
1001
1046
|
}
|
|
1002
1047
|
|
|
1003
|
-
// Step
|
|
1004
|
-
logStep('
|
|
1048
|
+
// Step 11: Install Claude Code hooks
|
|
1049
|
+
logStep('11/13', 'Installing Claude Code hooks...');
|
|
1005
1050
|
const HOOKS_DIR = path.join(CLAUDE_DIR, 'hooks');
|
|
1006
1051
|
const hooksSrc = path.join(filesDir, 'hooks');
|
|
1007
1052
|
if (fs.existsSync(hooksSrc)) {
|
|
@@ -1100,6 +1145,27 @@ async function install() {
|
|
|
1100
1145
|
hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/session-end.sh' }]
|
|
1101
1146
|
});
|
|
1102
1147
|
|
|
1148
|
+
// Stop hook: desktop notification (fires after session-end analytics)
|
|
1149
|
+
addHookIfMissing(settings.hooks.Stop, {
|
|
1150
|
+
hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/notify-desktop.sh' }]
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
// Stop hook: webhook notification (Slack / Discord, fire-and-forget)
|
|
1154
|
+
addHookIfMissing(settings.hooks.Stop, {
|
|
1155
|
+
hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/notify-webhook.sh' }]
|
|
1156
|
+
});
|
|
1157
|
+
|
|
1158
|
+
// Stop hook: auto-memory sync (extracts session decisions → Sights cloud)
|
|
1159
|
+
addHookIfMissing(settings.hooks.Stop, {
|
|
1160
|
+
hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/session-end-memory-sync.sh' }]
|
|
1161
|
+
});
|
|
1162
|
+
|
|
1163
|
+
// Notification hook: desktop alert when Claude needs input
|
|
1164
|
+
settings.hooks.Notification = settings.hooks.Notification || [];
|
|
1165
|
+
addHookIfMissing(settings.hooks.Notification, {
|
|
1166
|
+
hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/notify-desktop.sh' }]
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1103
1169
|
// TeammateIdle hook (Agent Teams quality gate — only fires when Teams active)
|
|
1104
1170
|
settings.hooks.TeammateIdle = settings.hooks.TeammateIdle || [];
|
|
1105
1171
|
addHookIfMissing(settings.hooks.TeammateIdle, {
|
|
@@ -1124,6 +1190,48 @@ async function install() {
|
|
|
1124
1190
|
hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/task-completed-verify.sh' }]
|
|
1125
1191
|
});
|
|
1126
1192
|
|
|
1193
|
+
// InstructionsLoaded hook (cold-start Sights pre-warm)
|
|
1194
|
+
settings.hooks.InstructionsLoaded = settings.hooks.InstructionsLoaded || [];
|
|
1195
|
+
addHookIfMissing(settings.hooks.InstructionsLoaded, {
|
|
1196
|
+
hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/instructions-loaded.sh' }]
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
// WorktreeCreate hook (propagate Merlin config into new worktrees)
|
|
1200
|
+
settings.hooks.WorktreeCreate = settings.hooks.WorktreeCreate || [];
|
|
1201
|
+
addHookIfMissing(settings.hooks.WorktreeCreate, {
|
|
1202
|
+
hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/worktree-create.sh' }]
|
|
1203
|
+
});
|
|
1204
|
+
|
|
1205
|
+
// WorktreeRemove hook (cleanup + lifecycle analytics)
|
|
1206
|
+
settings.hooks.WorktreeRemove = settings.hooks.WorktreeRemove || [];
|
|
1207
|
+
addHookIfMissing(settings.hooks.WorktreeRemove, {
|
|
1208
|
+
hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/worktree-remove.sh' }]
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
// ConfigChange hook (validate API key + audit trail)
|
|
1212
|
+
settings.hooks.ConfigChange = settings.hooks.ConfigChange || [];
|
|
1213
|
+
addHookIfMissing(settings.hooks.ConfigChange, {
|
|
1214
|
+
hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/config-change.sh' }]
|
|
1215
|
+
});
|
|
1216
|
+
|
|
1217
|
+
// PostToolUse: file size enforcement (implementation agents only)
|
|
1218
|
+
addHookIfMissing(settings.hooks.PostToolUse, {
|
|
1219
|
+
matcher: 'Edit|Write',
|
|
1220
|
+
hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/check-file-size.sh' }]
|
|
1221
|
+
});
|
|
1222
|
+
|
|
1223
|
+
// PreToolUse: security scanner (prompt injection, secrets, data exfiltration)
|
|
1224
|
+
addHookIfMissing(settings.hooks.PreToolUse, {
|
|
1225
|
+
matcher: 'Write|Edit|Bash|NotebookEdit',
|
|
1226
|
+
hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/security-scanner.sh' }]
|
|
1227
|
+
});
|
|
1228
|
+
|
|
1229
|
+
// PreToolUse: smart bash auto-approval (read-only safe, dangerous blocked, unknown pass-through)
|
|
1230
|
+
addHookIfMissing(settings.hooks.PreToolUse, {
|
|
1231
|
+
matcher: 'Bash',
|
|
1232
|
+
hooks: [{ type: 'command', command: 'bash ~/.claude/hooks/smart-approve.sh' }]
|
|
1233
|
+
});
|
|
1234
|
+
|
|
1127
1235
|
// --- Prompt-based hooks cleanup & registration ---
|
|
1128
1236
|
// Remove ALL prompt-type hooks from Merlin (identified by type: 'prompt').
|
|
1129
1237
|
// This prevents duplicates across version upgrades and removes broken hooks.
|
|
@@ -1144,6 +1252,10 @@ async function install() {
|
|
|
1144
1252
|
settings.hooks.Notification = removeAllPromptHooks(settings.hooks.Notification);
|
|
1145
1253
|
settings.hooks.TaskCompleted = settings.hooks.TaskCompleted || [];
|
|
1146
1254
|
settings.hooks.TaskCompleted = removeAllPromptHooks(settings.hooks.TaskCompleted);
|
|
1255
|
+
settings.hooks.InstructionsLoaded = removeAllPromptHooks(settings.hooks.InstructionsLoaded || []);
|
|
1256
|
+
settings.hooks.WorktreeCreate = removeAllPromptHooks(settings.hooks.WorktreeCreate || []);
|
|
1257
|
+
settings.hooks.WorktreeRemove = removeAllPromptHooks(settings.hooks.WorktreeRemove || []);
|
|
1258
|
+
settings.hooks.ConfigChange = removeAllPromptHooks(settings.hooks.ConfigChange || []);
|
|
1147
1259
|
|
|
1148
1260
|
// NOTE: The PreToolUse prompt hook has been REMOVED. It caused an infinite
|
|
1149
1261
|
// rejection loop because the evaluator model cannot see conversation history
|
|
@@ -1189,8 +1301,8 @@ async function install() {
|
|
|
1189
1301
|
return cfg;
|
|
1190
1302
|
}
|
|
1191
1303
|
|
|
1192
|
-
// Step
|
|
1193
|
-
logStep('
|
|
1304
|
+
// Step 12: Optional Merlin Sights configuration
|
|
1305
|
+
logStep('12/13', 'Merlin Sights configuration...');
|
|
1194
1306
|
|
|
1195
1307
|
// Check if API key is already configured (skip prompt on updates)
|
|
1196
1308
|
let existingApiKey = '';
|
|
@@ -1321,8 +1433,20 @@ async function install() {
|
|
|
1321
1433
|
logWarn('Skipped Merlin Sights (you can configure it later)');
|
|
1322
1434
|
}
|
|
1323
1435
|
|
|
1324
|
-
// Step
|
|
1325
|
-
logStep('
|
|
1436
|
+
// Step 13: Configure non-Claude-Code runtimes
|
|
1437
|
+
logStep('13/13', 'Configuring additional runtimes...');
|
|
1438
|
+
configureRuntimes({
|
|
1439
|
+
runtimeFlag: RUNTIME_FLAG,
|
|
1440
|
+
useGlobalBinary,
|
|
1441
|
+
apiKey,
|
|
1442
|
+
logSuccess,
|
|
1443
|
+
logWarn,
|
|
1444
|
+
logInfo: (msg) => log(msg),
|
|
1445
|
+
quiet: true, // step 2 already printed the detection summary
|
|
1446
|
+
});
|
|
1447
|
+
|
|
1448
|
+
// Shell integration (runs after runtime adapters)
|
|
1449
|
+
log(`\n${colors.cyan}[shell]${colors.reset} Setting up shell integration...`);
|
|
1326
1450
|
const shellConfigured = setupShellIntegration();
|
|
1327
1451
|
if (shellConfigured) {
|
|
1328
1452
|
log(`\n ${colors.yellow}IMPORTANT:${colors.reset} Run ${colors.cyan}source ~/.zshrc${colors.reset} or ${colors.bright}restart your terminal${colors.reset}`);
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
// MERLIN RUNTIME ADAPTERS
|
|
2
|
+
// Detects and configures Merlin for non-Claude-Code runtimes.
|
|
3
|
+
// Claude Code is always handled by the main installer (install.cjs).
|
|
4
|
+
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const os = require('os');
|
|
10
|
+
const { execSync } = require('child_process');
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Runtime definitions
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
const RUNTIMES = [
|
|
17
|
+
{
|
|
18
|
+
id: 'codex',
|
|
19
|
+
label: 'Codex CLI',
|
|
20
|
+
configDir: path.join(os.homedir(), '.codex'),
|
|
21
|
+
binary: 'codex',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'opencode',
|
|
25
|
+
label: 'OpenCode',
|
|
26
|
+
configDir: path.join(os.homedir(), '.opencode'),
|
|
27
|
+
binary: 'opencode',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
id: 'gemini',
|
|
31
|
+
label: 'Gemini CLI',
|
|
32
|
+
configDir: path.join(os.homedir(), '.gemini'),
|
|
33
|
+
binary: 'gemini',
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Detection helpers
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
function inPath(binary) {
|
|
42
|
+
try {
|
|
43
|
+
execSync(`which ${binary} 2>/dev/null`, { stdio: 'pipe' });
|
|
44
|
+
return true;
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function detectRuntimes() {
|
|
51
|
+
return RUNTIMES.map((rt) => ({
|
|
52
|
+
...rt,
|
|
53
|
+
found: fs.existsSync(rt.configDir) || inPath(rt.binary),
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// MCP server command helper (mirrors logic in install.cjs)
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
function buildMcpCommand(useGlobalBinary) {
|
|
62
|
+
if (useGlobalBinary) {
|
|
63
|
+
return { command: 'merlin-brain', args: undefined };
|
|
64
|
+
}
|
|
65
|
+
return { command: 'node', args: [path.join(__dirname, 'serve.js')] };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// AGENTS.md content for instruction-file runtimes
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
function buildAgentsMd() {
|
|
73
|
+
return `# Merlin Brain — AI Development System
|
|
74
|
+
|
|
75
|
+
> Installed by Merlin (https://merlin.build). Keep this file to preserve Merlin context.
|
|
76
|
+
>
|
|
77
|
+
> Note: Tool names in these instructions reflect Claude Code conventions (Read, Write, Edit,
|
|
78
|
+
> Bash, Grep, Glob). Names may differ in your runtime — adapt as needed.
|
|
79
|
+
|
|
80
|
+
## Boot Sequence (run at session start)
|
|
81
|
+
|
|
82
|
+
1. Call \`merlin_get_selected_repo\` — connect Merlin Sights to this project.
|
|
83
|
+
2. Call \`merlin_get_project_status\` — load current project state and active tasks.
|
|
84
|
+
3. Show a brief status summary, then handle the user's request.
|
|
85
|
+
|
|
86
|
+
Do NOT skip these steps. Do NOT start working without Merlin context.
|
|
87
|
+
|
|
88
|
+
## Before Every File Edit
|
|
89
|
+
|
|
90
|
+
Call \`merlin_get_context("your task")\` before writing or modifying any code.
|
|
91
|
+
|
|
92
|
+
## Routing Specialist Work
|
|
93
|
+
|
|
94
|
+
Route tasks to the right specialist agent:
|
|
95
|
+
- Architecture decisions → system-architect
|
|
96
|
+
- Implementation → implementation-dev
|
|
97
|
+
- Testing → tests-qa
|
|
98
|
+
- Security review → merlin-security
|
|
99
|
+
- Documentation → docs-keeper
|
|
100
|
+
|
|
101
|
+
## MCP Tools Available
|
|
102
|
+
|
|
103
|
+
The Merlin MCP server exposes these tools:
|
|
104
|
+
- \`merlin_get_selected_repo\` — identify current repository
|
|
105
|
+
- \`merlin_get_project_status\` — load tasks and project state
|
|
106
|
+
- \`merlin_get_context(task)\` — fetch relevant code context for a task
|
|
107
|
+
- \`merlin_find_files(query)\` — locate files by description
|
|
108
|
+
- \`merlin_search(query)\` — semantic code search
|
|
109
|
+
|
|
110
|
+
## Core Principles
|
|
111
|
+
|
|
112
|
+
- Always search before writing — reuse existing functions and patterns.
|
|
113
|
+
- Keep files under 400 lines — split into sub-modules when approaching the limit.
|
|
114
|
+
- Graceful failure — if Merlin Sights is unavailable, continue with file exploration.
|
|
115
|
+
`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// Codex CLI adapter
|
|
120
|
+
// Writes: ~/.codex/AGENTS.md + ~/.codex/config.toml (MCP section)
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
function configureCodex(rt, useGlobalBinary, apiKey) {
|
|
124
|
+
const results = [];
|
|
125
|
+
|
|
126
|
+
// Ensure config dir exists
|
|
127
|
+
if (!fs.existsSync(rt.configDir)) {
|
|
128
|
+
fs.mkdirSync(rt.configDir, { recursive: true });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Write AGENTS.md
|
|
132
|
+
const agentsMdPath = path.join(rt.configDir, 'AGENTS.md');
|
|
133
|
+
const agentsMd = buildAgentsMd();
|
|
134
|
+
const existingAgentsMd = fs.existsSync(agentsMdPath)
|
|
135
|
+
? fs.readFileSync(agentsMdPath, 'utf8')
|
|
136
|
+
: '';
|
|
137
|
+
|
|
138
|
+
if (existingAgentsMd.includes('Merlin Brain')) {
|
|
139
|
+
results.push('AGENTS.md already has Merlin (skipped)');
|
|
140
|
+
} else {
|
|
141
|
+
const combined = existingAgentsMd
|
|
142
|
+
? existingAgentsMd.trimEnd() + '\n\n---\n\n' + agentsMd
|
|
143
|
+
: agentsMd;
|
|
144
|
+
fs.writeFileSync(agentsMdPath, combined);
|
|
145
|
+
results.push('Wrote ~/.codex/AGENTS.md');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Write / update config.toml with MCP section
|
|
149
|
+
const configTomlPath = path.join(rt.configDir, 'config.toml');
|
|
150
|
+
const { command, args } = buildMcpCommand(useGlobalBinary);
|
|
151
|
+
const argsToml = args
|
|
152
|
+
? `args = [${args.map((a) => `"${a}"`).join(', ')}]`
|
|
153
|
+
: '';
|
|
154
|
+
const envToml = apiKey ? `\n MERLIN_API_KEY = "${apiKey}"` : '';
|
|
155
|
+
const mcpToml = `
|
|
156
|
+
[mcp.merlin]
|
|
157
|
+
command = "${command}"
|
|
158
|
+
${argsToml ? argsToml + '\n' : ''}${apiKey ? `[mcp.merlin.env]${envToml}\n` : ''}`;
|
|
159
|
+
|
|
160
|
+
let tomlContent = fs.existsSync(configTomlPath)
|
|
161
|
+
? fs.readFileSync(configTomlPath, 'utf8')
|
|
162
|
+
: '';
|
|
163
|
+
|
|
164
|
+
if (tomlContent.includes('[mcp.merlin]')) {
|
|
165
|
+
results.push('config.toml already has Merlin MCP (skipped)');
|
|
166
|
+
} else {
|
|
167
|
+
fs.appendFileSync(configTomlPath, mcpToml);
|
|
168
|
+
results.push('Added Merlin MCP to ~/.codex/config.toml');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return results;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// OpenCode adapter
|
|
176
|
+
// Writes: ~/.opencode/config.json (mcpServers section) + ~/.opencode/AGENTS.md
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
function configureOpenCode(rt, useGlobalBinary, apiKey) {
|
|
180
|
+
const results = [];
|
|
181
|
+
|
|
182
|
+
if (!fs.existsSync(rt.configDir)) {
|
|
183
|
+
fs.mkdirSync(rt.configDir, { recursive: true });
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// MCP config JSON
|
|
187
|
+
const configJsonPath = path.join(rt.configDir, 'config.json');
|
|
188
|
+
let config = {};
|
|
189
|
+
if (fs.existsSync(configJsonPath)) {
|
|
190
|
+
try {
|
|
191
|
+
config = JSON.parse(fs.readFileSync(configJsonPath, 'utf8'));
|
|
192
|
+
} catch {
|
|
193
|
+
config = {};
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
config.mcpServers = config.mcpServers || {};
|
|
198
|
+
if (config.mcpServers.merlin) {
|
|
199
|
+
results.push('config.json already has Merlin MCP (skipped)');
|
|
200
|
+
} else {
|
|
201
|
+
const { command, args } = buildMcpCommand(useGlobalBinary);
|
|
202
|
+
const mcpEntry = { command, type: 'stdio' };
|
|
203
|
+
if (args) mcpEntry.args = args;
|
|
204
|
+
if (apiKey) mcpEntry.env = { MERLIN_API_KEY: apiKey };
|
|
205
|
+
config.mcpServers.merlin = mcpEntry;
|
|
206
|
+
fs.writeFileSync(configJsonPath, JSON.stringify(config, null, 2));
|
|
207
|
+
results.push('Added Merlin MCP to ~/.opencode/config.json');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Instructions file
|
|
211
|
+
const agentsMdPath = path.join(rt.configDir, 'AGENTS.md');
|
|
212
|
+
const existingAgentsMd = fs.existsSync(agentsMdPath)
|
|
213
|
+
? fs.readFileSync(agentsMdPath, 'utf8')
|
|
214
|
+
: '';
|
|
215
|
+
if (existingAgentsMd.includes('Merlin Brain')) {
|
|
216
|
+
results.push('AGENTS.md already has Merlin (skipped)');
|
|
217
|
+
} else {
|
|
218
|
+
const combined = existingAgentsMd
|
|
219
|
+
? existingAgentsMd.trimEnd() + '\n\n---\n\n' + buildAgentsMd()
|
|
220
|
+
: buildAgentsMd();
|
|
221
|
+
fs.writeFileSync(agentsMdPath, combined);
|
|
222
|
+
results.push('Wrote ~/.opencode/AGENTS.md');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return results;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ---------------------------------------------------------------------------
|
|
229
|
+
// Gemini CLI adapter
|
|
230
|
+
// Writes: ~/.gemini/GEMINI.md + ~/.gemini/settings.json (mcpServers section)
|
|
231
|
+
// ---------------------------------------------------------------------------
|
|
232
|
+
|
|
233
|
+
function configureGemini(rt, useGlobalBinary, apiKey) {
|
|
234
|
+
const results = [];
|
|
235
|
+
|
|
236
|
+
if (!fs.existsSync(rt.configDir)) {
|
|
237
|
+
fs.mkdirSync(rt.configDir, { recursive: true });
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// GEMINI.md instructions file
|
|
241
|
+
const geminiMdPath = path.join(rt.configDir, 'GEMINI.md');
|
|
242
|
+
const existingGeminiMd = fs.existsSync(geminiMdPath)
|
|
243
|
+
? fs.readFileSync(geminiMdPath, 'utf8')
|
|
244
|
+
: '';
|
|
245
|
+
if (existingGeminiMd.includes('Merlin Brain')) {
|
|
246
|
+
results.push('GEMINI.md already has Merlin (skipped)');
|
|
247
|
+
} else {
|
|
248
|
+
const combined = existingGeminiMd
|
|
249
|
+
? existingGeminiMd.trimEnd() + '\n\n---\n\n' + buildAgentsMd()
|
|
250
|
+
: buildAgentsMd();
|
|
251
|
+
fs.writeFileSync(geminiMdPath, combined);
|
|
252
|
+
results.push('Wrote ~/.gemini/GEMINI.md');
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// settings.json MCP config
|
|
256
|
+
const settingsPath = path.join(rt.configDir, 'settings.json');
|
|
257
|
+
let settings = {};
|
|
258
|
+
if (fs.existsSync(settingsPath)) {
|
|
259
|
+
try {
|
|
260
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
261
|
+
} catch {
|
|
262
|
+
settings = {};
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
settings.mcpServers = settings.mcpServers || {};
|
|
267
|
+
if (settings.mcpServers.merlin) {
|
|
268
|
+
results.push('settings.json already has Merlin MCP (skipped)');
|
|
269
|
+
} else {
|
|
270
|
+
const { command, args } = buildMcpCommand(useGlobalBinary);
|
|
271
|
+
const mcpEntry = { command };
|
|
272
|
+
if (args) mcpEntry.args = args;
|
|
273
|
+
if (apiKey) mcpEntry.env = { MERLIN_API_KEY: apiKey };
|
|
274
|
+
settings.mcpServers.merlin = mcpEntry;
|
|
275
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
|
|
276
|
+
results.push('Added Merlin MCP to ~/.gemini/settings.json');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return results;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
// Per-project runtime files
|
|
284
|
+
// Writes AGENTS.md alongside CLAUDE.md when initializing a project.
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Generate per-project runtime instruction files next to a CLAUDE.md.
|
|
289
|
+
* Call this whenever Merlin creates or updates a project's CLAUDE.md.
|
|
290
|
+
*
|
|
291
|
+
* @param {string} projectDir - Absolute path to the project directory
|
|
292
|
+
* @param {string[]} runtimeIds - Which runtime files to generate ('codex'|'opencode'|'gemini'|'all')
|
|
293
|
+
*/
|
|
294
|
+
function generateProjectRuntimeFiles(projectDir, runtimeIds = ['all']) {
|
|
295
|
+
const results = [];
|
|
296
|
+
const targets = runtimeIds.includes('all')
|
|
297
|
+
? ['codex', 'opencode', 'gemini']
|
|
298
|
+
: runtimeIds;
|
|
299
|
+
|
|
300
|
+
const agentsMd = buildAgentsMd();
|
|
301
|
+
|
|
302
|
+
// AGENTS.md — used by Codex and OpenCode
|
|
303
|
+
if (targets.includes('codex') || targets.includes('opencode')) {
|
|
304
|
+
const agentsMdPath = path.join(projectDir, 'AGENTS.md');
|
|
305
|
+
if (!fs.existsSync(agentsMdPath)) {
|
|
306
|
+
fs.writeFileSync(agentsMdPath, agentsMd);
|
|
307
|
+
results.push(`Created ${agentsMdPath}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// GEMINI.md — used by Gemini CLI
|
|
312
|
+
if (targets.includes('gemini')) {
|
|
313
|
+
const geminiMdPath = path.join(projectDir, 'GEMINI.md');
|
|
314
|
+
if (!fs.existsSync(geminiMdPath)) {
|
|
315
|
+
fs.writeFileSync(geminiMdPath, agentsMd);
|
|
316
|
+
results.push(`Created ${geminiMdPath}`);
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return results;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// ---------------------------------------------------------------------------
|
|
324
|
+
// Main entry point called from install.cjs
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Detect and configure all non-Claude-Code runtimes.
|
|
329
|
+
*
|
|
330
|
+
* @param {object} opts
|
|
331
|
+
* @param {string} opts.runtimeFlag - Value of --runtime flag ('all'|'claude'|runtime-id)
|
|
332
|
+
* @param {boolean} opts.useGlobalBinary - Whether merlin-brain global binary is available
|
|
333
|
+
* @param {string} opts.apiKey - Merlin Sights API key (may be empty)
|
|
334
|
+
* @param {Function} opts.logSuccess - Logging helper
|
|
335
|
+
* @param {Function} opts.logWarn - Logging helper
|
|
336
|
+
* @param {Function} opts.logInfo - Plain log helper (console.log compatible)
|
|
337
|
+
* @param {boolean} [opts.quiet] - If true, skip printing the detection summary
|
|
338
|
+
* @returns {{ detected: object[], configured: string[] }}
|
|
339
|
+
*/
|
|
340
|
+
function configureRuntimes({ runtimeFlag, useGlobalBinary, apiKey, logSuccess, logWarn, logInfo, quiet }) {
|
|
341
|
+
const detected = detectRuntimes();
|
|
342
|
+
|
|
343
|
+
// Print detection summary (skipped when caller already printed it)
|
|
344
|
+
if (!quiet) {
|
|
345
|
+
for (const rt of detected) {
|
|
346
|
+
const icon = rt.found ? '\u2705' : '\u2B1A';
|
|
347
|
+
const suffix = rt.found ? '' : ' (not found)';
|
|
348
|
+
logInfo(` ${icon} ${rt.label}${suffix}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Determine which runtimes to configure
|
|
353
|
+
const skip = runtimeFlag === 'claude'; // --runtime claude → Claude Code only
|
|
354
|
+
if (skip) {
|
|
355
|
+
if (!quiet) logInfo(' Skipping other runtimes (--runtime claude)');
|
|
356
|
+
return { detected, configured: [] };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const filterToOne = !['all', 'claude', undefined, ''].includes(runtimeFlag);
|
|
360
|
+
const targetRuntimes = filterToOne
|
|
361
|
+
? detected.filter((rt) => rt.found && rt.id === runtimeFlag)
|
|
362
|
+
: detected.filter((rt) => rt.found);
|
|
363
|
+
|
|
364
|
+
const configured = [];
|
|
365
|
+
|
|
366
|
+
for (const rt of targetRuntimes) {
|
|
367
|
+
let results = [];
|
|
368
|
+
try {
|
|
369
|
+
if (rt.id === 'codex') {
|
|
370
|
+
results = configureCodex(rt, useGlobalBinary, apiKey);
|
|
371
|
+
} else if (rt.id === 'opencode') {
|
|
372
|
+
results = configureOpenCode(rt, useGlobalBinary, apiKey);
|
|
373
|
+
} else if (rt.id === 'gemini') {
|
|
374
|
+
results = configureGemini(rt, useGlobalBinary, apiKey);
|
|
375
|
+
} else {
|
|
376
|
+
logWarn(` Unknown runtime "${rt.id}" — skipped`);
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
for (const msg of results) {
|
|
380
|
+
logSuccess(msg);
|
|
381
|
+
configured.push(msg);
|
|
382
|
+
}
|
|
383
|
+
} catch (err) {
|
|
384
|
+
logWarn(` Failed to configure ${rt.label}: ${err.message}`);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return { detected, configured };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
module.exports = {
|
|
392
|
+
detectRuntimes,
|
|
393
|
+
configureRuntimes,
|
|
394
|
+
generateProjectRuntimeFiles,
|
|
395
|
+
buildAgentsMd,
|
|
396
|
+
};
|