From 0998e5d94aac5930024effd991a8042839b6516b Mon Sep 17 00:00:00 2001 From: whickerpoplawski-arch Date: Thu, 21 May 2026 15:06:27 +0800 Subject: [PATCH 1/3] feat(installer): add Devin for Terminal target --- src/installer/targets/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/installer/targets/types.ts b/src/installer/targets/types.ts index fdff0d77..1c167e95 100644 --- a/src/installer/targets/types.ts +++ b/src/installer/targets/types.ts @@ -19,7 +19,7 @@ export type Location = 'global' | 'local'; * lookup. New targets add a value here when they're added to the * registry. Keep these short and lowercase. */ -export type TargetId = 'claude' | 'cursor' | 'codex' | 'opencode'; +export type export type TargetId = 'claude' | 'cursor' | 'codex' | 'devin' | 'opencode'; /** * Result of `target.detect(location)`. From 9371827d87da20476c19b8f88ca83d3893879240 Mon Sep 17 00:00:00 2001 From: whickerpoplawski-arch Date: Thu, 21 May 2026 15:08:59 +0800 Subject: [PATCH 2/3] feat(installer): add Devin for Terminal target --- src/installer/targets/registry.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/installer/targets/registry.ts b/src/installer/targets/registry.ts index e671fd19..e8f261a7 100644 --- a/src/installer/targets/registry.ts +++ b/src/installer/targets/registry.ts @@ -11,12 +11,14 @@ import { AgentTarget, Location, TargetId } from './types'; import { claudeTarget } from './claude'; import { cursorTarget } from './cursor'; import { codexTarget } from './codex'; +import { devinTarget } from './devin'; import { opencodeTarget } from './opencode'; export const ALL_TARGETS: readonly AgentTarget[] = Object.freeze([ claudeTarget, cursorTarget, codexTarget, + devinTarget, opencodeTarget, ]); From 648da3d05f9f64a518ae148521e969067ecf4d22 Mon Sep 17 00:00:00 2001 From: whickerpoplawski-arch Date: Thu, 21 May 2026 15:10:26 +0800 Subject: [PATCH 3/3] feat(installer): add Devin for Terminal target --- src/installer/targets/devin.ts | 156 +++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 src/installer/targets/devin.ts diff --git a/src/installer/targets/devin.ts b/src/installer/targets/devin.ts new file mode 100644 index 00000000..0a815d2a --- /dev/null +++ b/src/installer/targets/devin.ts @@ -0,0 +1,156 @@ +/** + * Devin for Terminal target. + * + * - User config: `~/.config/devin/config.json` (POSIX) or + * `%APPDATA%/devin/config.json` (Windows). + * - Project config: `./.devin/config.json`. + * - MCP config goes under `mcpServers.codegraph` with the stdio + * block `{ command: 'codegraph', args: ['serve', '--mcp'] }`. + * - Devin reads `AGENTS.md`, so global writes `~/.config/devin/AGENTS.md` + * and local writes `./AGENTS.md`. + * - No permissions concept. + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; +import { + AgentTarget, + DetectionResult, + InstallOptions, + Location, + WriteResult, +} from './types'; +import { + getMcpServerConfig, + jsonDeepEqual, + removeMarkedSection, + replaceOrAppendMarkedSection, + writeJsonFile, +} from './shared'; +import { + CODEGRAPH_SECTION_END, + CODEGRAPH_SECTION_START, + INSTRUCTIONS_TEMPLATE, +} from '../instructions-template'; + +function globalConfigDir(): string { + if (process.platform === 'win32') { + const appData = process.env.APPDATA ?? path.join(os.homedir(), 'AppData', 'Roaming'); + return path.join(appData, 'devin'); + } + return path.join(os.homedir(), '.config', 'devin'); +} + +function configPath(loc: Location): string { + return path.join(loc === 'global' ? globalConfigDir() : path.join(process.cwd(), '.devin'), 'config.json'); +} + +function instructionsPath(loc: Location): string { + return path.join(loc === 'global' ? globalConfigDir() : process.cwd(), 'AGENTS.md'); +} + +function readConfig(file: string): Record { + if (!fs.existsSync(file)) return {}; + try { + const text = fs.readFileSync(file, 'utf-8'); + const parsed = JSON.parse(text); + return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {}; + } catch { + return {}; + } +} + +class DevinTarget implements AgentTarget { + readonly id = 'devin' as const; + readonly displayName = 'Devin for Terminal'; + + supportsLocation(_loc: Location): boolean { + return true; + } + + detect(loc: Location): DetectionResult { + const file = configPath(loc); + const config = readConfig(file); + const alreadyConfigured = !!config.mcpServers?.codegraph; + const installed = loc === 'global' + ? fs.existsSync(globalConfigDir()) + : fs.existsSync(path.join(process.cwd(), '.devin')); + return { installed, alreadyConfigured, configPath: file }; + } + + install(loc: Location, _opts: InstallOptions): WriteResult { + return { + files: [writeMcpEntry(loc), writeInstructionsEntry(loc)], + }; + } + + uninstall(loc: Location): WriteResult { + const files: WriteResult['files'] = []; + const file = configPath(loc); + if (!fs.existsSync(file)) { + files.push({ path: file, action: 'not-found' }); + } else { + const config = readConfig(file); + if (!config.mcpServers?.codegraph) { + files.push({ path: file, action: 'not-found' }); + } else { + delete config.mcpServers.codegraph; + if (Object.keys(config.mcpServers).length === 0) { + delete config.mcpServers; + } + writeJsonFile(file, config); + files.push({ path: file, action: 'removed' }); + } + } + + files.push({ + path: instructionsPath(loc), + action: removeMarkedSection(instructionsPath(loc), CODEGRAPH_SECTION_START, CODEGRAPH_SECTION_END), + }); + return { files }; + } + + printConfig(loc: Location): string { + return `# Add to ${configPath(loc)}\n\n${JSON.stringify({ + mcpServers: { codegraph: getMcpServerConfig() }, + }, null, 2)}\n`; + } + + describePaths(loc: Location): string[] { + return [configPath(loc), instructionsPath(loc)]; + } +} + +function writeMcpEntry(loc: Location): WriteResult['files'][number] { + const file = configPath(loc); + const existed = fs.existsSync(file); + const config = readConfig(file); + const before = config.mcpServers?.codegraph; + const after = getMcpServerConfig(); + + if (jsonDeepEqual(before, after)) { + return { path: file, action: 'unchanged' }; + } + + if (!config.mcpServers) config.mcpServers = {}; + config.mcpServers.codegraph = after; + writeJsonFile(file, config); + return { path: file, action: existed ? 'updated' : 'created' }; +} + +function writeInstructionsEntry(loc: Location): WriteResult['files'][number] { + const file = instructionsPath(loc); + const action = replaceOrAppendMarkedSection( + file, + INSTRUCTIONS_TEMPLATE, + CODEGRAPH_SECTION_START, + CODEGRAPH_SECTION_END, + ); + return { + path: file, + action: action === 'created' ? 'created' : action === 'unchanged' ? 'unchanged' : 'updated', + }; +} + +export const devinTarget: AgentTarget = new DevinTarget();