@ai-devkit/agent-manager 0.38.0 → 0.39.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/dist/__tests__/harnesses/devin/DevinAdapter.test.js +597 -0
- package/dist/__tests__/harnesses/devin/DevinAdapter.test.js.map +1 -0
- package/dist/__tests__/harnesses/devin/DevinAgentMapper.test.js +91 -0
- package/dist/__tests__/harnesses/devin/DevinAgentMapper.test.js.map +1 -0
- package/dist/__tests__/harnesses/devin/DevinSessionLocator.test.js +192 -0
- package/dist/__tests__/harnesses/devin/DevinSessionLocator.test.js.map +1 -0
- package/dist/__tests__/harnesses/devin/DevinSessionParser.test.js +407 -0
- package/dist/__tests__/harnesses/devin/DevinSessionParser.test.js.map +1 -0
- package/dist/__tests__/harnesses/devin/fixtures.js +95 -0
- package/dist/__tests__/harnesses/devin/fixtures.js.map +1 -0
- package/dist/__tests__/harnesses/readiness/AgentReadiness.test.js +64 -1
- package/dist/__tests__/harnesses/readiness/AgentReadiness.test.js.map +1 -1
- package/dist/__tests__/terminal/GhosttyBackend.test.js +334 -0
- package/dist/__tests__/terminal/GhosttyBackend.test.js.map +1 -0
- package/dist/adapters/AgentAdapter.d.ts +1 -1
- package/dist/adapters/AgentAdapter.d.ts.map +1 -1
- package/dist/adapters/AgentAdapter.js +2 -1
- package/dist/adapters/AgentAdapter.js.map +1 -1
- package/dist/harnesses/devin/DevinAdapter.d.ts +39 -0
- package/dist/harnesses/devin/DevinAdapter.d.ts.map +1 -0
- package/dist/harnesses/devin/DevinAdapter.js +177 -0
- package/dist/harnesses/devin/DevinAdapter.js.map +1 -0
- package/dist/harnesses/devin/DevinAgentMapper.d.ts +11 -0
- package/dist/harnesses/devin/DevinAgentMapper.d.ts.map +1 -0
- package/dist/harnesses/devin/DevinAgentMapper.js +35 -0
- package/dist/harnesses/devin/DevinAgentMapper.js.map +1 -0
- package/dist/harnesses/devin/DevinSessionLocator.d.ts +46 -0
- package/dist/harnesses/devin/DevinSessionLocator.d.ts.map +1 -0
- package/dist/harnesses/devin/DevinSessionLocator.js +176 -0
- package/dist/harnesses/devin/DevinSessionLocator.js.map +1 -0
- package/dist/harnesses/devin/DevinSessionParser.d.ts +29 -0
- package/dist/harnesses/devin/DevinSessionParser.d.ts.map +1 -0
- package/dist/harnesses/devin/DevinSessionParser.js +166 -0
- package/dist/harnesses/devin/DevinSessionParser.js.map +1 -0
- package/dist/harnesses/devin/readiness.d.ts +3 -0
- package/dist/harnesses/devin/readiness.d.ts.map +1 -0
- package/dist/harnesses/devin/readiness.js +39 -0
- package/dist/harnesses/devin/readiness.js.map +1 -0
- package/dist/harnesses/index.d.ts.map +1 -1
- package/dist/harnesses/index.js +3 -1
- package/dist/harnesses/index.js.map +1 -1
- package/dist/harnesses/readiness/AgentReadiness.d.ts.map +1 -1
- package/dist/harnesses/readiness/AgentReadiness.js +3 -1
- package/dist/harnesses/readiness/AgentReadiness.js.map +1 -1
- package/dist/harnesses/readiness/types.d.ts +1 -1
- package/dist/harnesses/readiness/types.d.ts.map +1 -1
- package/dist/harnesses/runtimeProfiles.d.ts.map +1 -1
- package/dist/harnesses/runtimeProfiles.js +2 -1
- package/dist/harnesses/runtimeProfiles.js.map +1 -1
- package/dist/terminal/TtyWriter.d.ts.map +1 -1
- package/dist/terminal/TtyWriter.js +4 -3
- package/dist/terminal/TtyWriter.js.map +1 -1
- package/dist/terminal/backends/ghostty.d.ts +3 -0
- package/dist/terminal/backends/ghostty.d.ts.map +1 -0
- package/dist/terminal/backends/ghostty.js +161 -0
- package/dist/terminal/backends/ghostty.js.map +1 -0
- package/dist/terminal/backends/index.d.ts.map +1 -1
- package/dist/terminal/backends/index.js +2 -0
- package/dist/terminal/backends/index.js.map +1 -1
- package/dist/terminal/backends/shared.d.ts +2 -0
- package/dist/terminal/backends/shared.d.ts.map +1 -1
- package/dist/terminal/backends/shared.js +4 -1
- package/dist/terminal/backends/shared.js.map +1 -1
- package/dist/terminal/types.d.ts +1 -0
- package/dist/terminal/types.d.ts.map +1 -1
- package/dist/terminal/types.js +1 -0
- package/dist/terminal/types.js.map +1 -1
- package/dist/utils/process.d.ts +2 -0
- package/dist/utils/process.d.ts.map +1 -1
- package/dist/utils/process.js +3 -0
- package/dist/utils/process.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/harnesses/devin/DevinAdapter.test.ts +292 -0
- package/src/__tests__/harnesses/devin/DevinAgentMapper.test.ts +88 -0
- package/src/__tests__/harnesses/devin/DevinSessionLocator.test.ts +139 -0
- package/src/__tests__/harnesses/devin/DevinSessionParser.test.ts +218 -0
- package/src/__tests__/harnesses/devin/fixtures.ts +144 -0
- package/src/__tests__/harnesses/readiness/AgentReadiness.test.ts +41 -0
- package/src/__tests__/terminal/GhosttyBackend.test.ts +271 -0
- package/src/adapters/AgentAdapter.ts +1 -0
- package/src/harnesses/devin/DevinAdapter.ts +224 -0
- package/src/harnesses/devin/DevinAgentMapper.ts +43 -0
- package/src/harnesses/devin/DevinSessionLocator.ts +230 -0
- package/src/harnesses/devin/DevinSessionParser.ts +236 -0
- package/src/harnesses/devin/readiness.ts +36 -0
- package/src/harnesses/index.ts +2 -0
- package/src/harnesses/readiness/AgentReadiness.ts +2 -0
- package/src/harnesses/runtimeProfiles.ts +1 -0
- package/src/terminal/TtyWriter.ts +5 -5
- package/src/terminal/backends/ghostty.ts +180 -0
- package/src/terminal/backends/index.ts +2 -0
- package/src/terminal/backends/shared.ts +6 -1
- package/src/terminal/types.ts +1 -0
- package/src/utils/process.ts +5 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/__tests__/harnesses/readiness/AgentReadiness.test.ts"],"sourcesContent":["import path from \"node:path\";\nimport { describe, expect, it, vi } from \"vitest\";\nimport {\n getAgentReadinessReport,\n getAgentReadinessReports,\n type AgentReadinessOptions,\n type ReadinessAgentType,\n} from \"../../../harnesses/readiness/AgentReadiness.js\";\nimport { HARNESS_RUNTIME_PROFILES } from \"../../../harnesses/runtimeProfiles.js\";\n\ntype Files = Record<string, string>;\n\nconst builtInSkillNames = [\"agent-management\", \"memory\", \"verify\"];\nconst skillRoots: Record<string, string> = {\n claude: \".claude/skills\",\n codex: \".codex/skills\",\n copilot: \".copilot/skills\",\n gemini_cli: \".gemini/skills\",\n grok_cli: \".grok/skills\",\n antigravity_cli: \".gemini/config/skills\",\n opencode: \".config/opencode/skill\",\n pi: \".pi/agent/skills\",\n};\n\nfunction fixture(overrides: Partial<AgentReadinessOptions> = {}) {\n const homeDir = \"/home/test\";\n const assetRoot = \"/assets\";\n const files: Files = {\n [path.join(homeDir, \".codex\", \"hooks\", \"codex-session-mapping.cjs\")]: \"codex-hook\",\n [path.join(assetRoot, \"codex\", \"codex-session-mapping.cjs\")]: \"codex-hook\",\n [path.join(homeDir, \".codex\", \"hooks.json\")]: JSON.stringify({\n hooks: {\n SessionStart: [\n {\n hooks: [{ type: \"command\", command: \"node ~/.codex/hooks/codex-session-mapping.cjs\" }],\n },\n ],\n },\n }),\n [path.join(homeDir, \".codex\", \"ai-devkit\", \"sessions.json\")]: JSON.stringify({\n \"123\": \"/sessions/codex.jsonl\",\n }),\n \"/sessions/codex.jsonl\": \"\",\n [path.join(homeDir, \".claude\", \"hooks\", \"claude-prompt-hook.js\")]: \"claude-hook\",\n [path.join(assetRoot, \"claude\", \"claude-prompt-hook.js\")]: \"claude-hook\",\n [path.join(homeDir, \".claude\", \"settings.json\")]: JSON.stringify({\n hooks: {\n PreToolUse: [\n { hooks: [{ type: \"command\", command: \"node ~/.claude/hooks/claude-prompt-hook.js\" }] },\n ],\n },\n }),\n [path.join(homeDir, \".pi\", \"agent\", \"sessions.json\")]: JSON.stringify({\n \"456\": \"/sessions/missing-pi.jsonl\",\n }),\n [path.join(homeDir, \".pi\", \"agent\", \"auth.json\")]: JSON.stringify({\n anthropic: { access: \"access-secret\", refresh: \"refresh-secret\" },\n litellm: { key: \"litellm-secret\", env: { LITELLM_BASE_URL: \"https://litellm.example.test\" } },\n }),\n };\n for (const directory of [\n \".claude\",\n \".codex\",\n \".copilot\",\n \".gemini\",\n \".grok\",\n \".kiro\",\n \".gemini/antigravity-cli\",\n \".config/opencode\",\n \".pi\",\n ]) {\n files[path.join(homeDir, directory)] = \"<dir>\";\n }\n for (const root of Object.values(skillRoots)) {\n for (const skill of builtInSkillNames) {\n files[path.join(homeDir, root, skill, \"SKILL.md\")] = \"# skill\";\n }\n }\n const executablePaths = Object.values(HARNESS_RUNTIME_PROFILES).map((agent) =>\n path.join(\"/bin\", agent.command),\n );\n const options: AgentReadinessOptions = {\n homeDir,\n path: \"/bin\",\n assetRoot,\n builtInSkillNames,\n skillRoots,\n readFile: async (target) => {\n if (!(target in files)) throw new Error(\"missing\");\n return files[target];\n },\n access: async (target) => {\n if (executablePaths.includes(target) || target in files) return;\n throw new Error(\"missing\");\n },\n runCommand: vi.fn(async (command) => {\n if (command === \"pi\") return { stdout: \"npm:@ai-devkit/pi-session-tracker\\n\", stderr: \"\" };\n if (command === \"claude\") return { stdout: JSON.stringify({ loggedIn: true }), stderr: \"\" };\n if (command === \"gh\")\n return { stdout: \"github.com\\n Logged in to github.com account test-user\\n\", stderr: \"\" };\n if (command === \"opencode\")\n return { stdout: \"● litellm api\\n● OpenAI oauth\\n\", stderr: \"\" };\n throw new Error(`unexpected command ${command}`);\n }),\n codexAuth: async () => true,\n ...overrides,\n };\n return { options, files };\n}\n\ndescribe(\"agent readiness\", () => {\n it(\"reports every readiness-supported adapter with generic checks and only real integrations\", async () => {\n const { options } = fixture();\n\n const reports = await getAgentReadinessReports(options);\n\n expect(Object.keys(reports)).toEqual([\n \"claude\",\n \"codex\",\n \"gemini_cli\",\n \"grok_cli\",\n \"kiro\",\n \"antigravity_cli\",\n \"opencode\",\n \"copilot\",\n \"pi\",\n ]);\n for (const [agent, report] of Object.entries(reports) as Array<\n [ReadinessAgentType, (typeof reports)[ReadinessAgentType]]\n >) {\n expect(report.type).toBe(agent);\n expect(report.executable.status).toBe(\"pass\");\n expect(report.globalConfig.status).toBe(\"pass\");\n expect(report.builtInSkills.status).toBe(\"info\");\n }\n expect(reports.gemini_cli.builtInSkills).toMatchObject({ present: 3, required: 3 });\n expect(reports.antigravity_cli.builtInSkills).toMatchObject({ present: 3, required: 3 });\n expect(reports.kiro.builtInSkills).toMatchObject({\n path: null,\n present: 0,\n required: 3,\n missing: builtInSkillNames,\n });\n expect(reports.codex.integration).toMatchObject({\n label: \"ai-devkit hook\",\n installed: true,\n status: \"pass\",\n });\n expect(reports.claude.integration).toMatchObject({\n label: \"ai-devkit hook\",\n installed: true,\n status: \"pass\",\n });\n expect(reports.pi.integration).toMatchObject({\n label: \"ai-devkit plugin\",\n installed: true,\n status: \"pass\",\n });\n expect(reports.copilot.integration).toBeUndefined();\n expect(reports.gemini_cli.integration).toBeUndefined();\n expect(reports.grok_cli.integration).toBeUndefined();\n expect(reports.kiro.integration).toBeUndefined();\n expect(reports.antigravity_cli.integration).toBeUndefined();\n expect(reports.opencode.integration).toBeUndefined();\n });\n\n it(\"resolves agent executables from PATH directories containing spaces\", async () => {\n const spacedBin = \"/Applications/Some App.app/Contents/Resources\";\n const codexPath = path.join(spacedBin, HARNESS_RUNTIME_PROFILES.codex.command);\n const { options } = fixture({\n path: [spacedBin, \"/usr/local/bin\"].join(path.delimiter),\n access: async (target) => {\n if (target === codexPath || target.startsWith(\"/home/test\")) return;\n throw new Error(\"missing\");\n },\n });\n\n const report = await getAgentReadinessReport(\"codex\", options);\n\n expect(report.executable).toMatchObject({ command: \"codex\", path: codexPath, status: \"pass\" });\n });\n\n it(\"parses Pi provider names without returning credential values\", async () => {\n const { options } = fixture();\n\n const report = await getAgentReadinessReport(\"pi\", options);\n const serialized = JSON.stringify(report);\n\n expect(report.auth).toMatchObject({\n state: \"authenticated\",\n provider: null,\n availableProviders: [\"anthropic\", \"litellm\"],\n status: \"pass\",\n });\n expect(serialized).not.toContain(\"access-secret\");\n expect(serialized).not.toContain(\"refresh-secret\");\n expect(serialized).not.toContain(\"litellm-secret\");\n expect(serialized).not.toContain(\"litellm.example.test\");\n });\n\n it(\"accepts Pi session tracker display names from pi list output\", async () => {\n const { options } = fixture({\n runCommand: vi.fn(async (command) => {\n if (command === \"pi\") return { stdout: \"session tracker\\n\", stderr: \"\" };\n if (command === \"claude\") return { stdout: JSON.stringify({ loggedIn: true }), stderr: \"\" };\n throw new Error(`unexpected command ${command}`);\n }),\n });\n\n const report = await getAgentReadinessReport(\"pi\", options);\n\n expect(report.integration).toMatchObject({\n label: \"ai-devkit plugin\",\n installed: true,\n status: \"pass\",\n });\n });\n\n it(\"reports missing built-in skills as informational readiness\", async () => {\n const { options, files } = fixture();\n delete files[path.join(options.homeDir!, skillRoots.codex, \"verify\", \"SKILL.md\")];\n\n const report = await getAgentReadinessReport(\"codex\", options);\n\n expect(report.builtInSkills).toMatchObject({\n status: \"info\",\n present: 2,\n required: 3,\n missing: [\"verify\"],\n });\n expect(report.status).toBe(\"pass\");\n });\n\n it(\"checks OpenCode auth from auth list output\", async () => {\n const { options } = fixture({\n runCommand: vi.fn(async (command) => {\n if (command === \"opencode\") {\n return {\n stdout: \"\\x1B[32m●\\x1B[0m litellm api\\n● OpenAI oauth\\n● GitLab Duo GITLAB_TOKEN\\n\",\n stderr: \"\",\n };\n }\n if (command === \"pi\") return { stdout: \"npm:@ai-devkit/pi-session-tracker\\n\", stderr: \"\" };\n if (command === \"claude\") return { stdout: JSON.stringify({ loggedIn: true }), stderr: \"\" };\n throw new Error(`unexpected command ${command}`);\n }),\n });\n\n const report = await getAgentReadinessReport(\"opencode\", options);\n\n expect(report.auth).toMatchObject({\n state: \"authenticated\",\n source: \"opencode auth list\",\n provider: null,\n availableProviders: [\"GitLab Duo\", \"OpenAI\", \"litellm\"],\n status: \"pass\",\n });\n });\n\n it(\"checks Copilot auth through GitHub CLI authentication\", async () => {\n const { options } = fixture({\n runCommand: vi.fn(async (command) => {\n if (command === \"gh\")\n return {\n stdout: \"github.com\\n Logged in to github.com account test-user\\n\",\n stderr: \"\",\n };\n if (command === \"pi\") return { stdout: \"npm:@ai-devkit/pi-session-tracker\\n\", stderr: \"\" };\n if (command === \"claude\") return { stdout: JSON.stringify({ loggedIn: true }), stderr: \"\" };\n if (command === \"opencode\") return { stdout: \"● litellm api\\n\", stderr: \"\" };\n throw new Error(`unexpected command ${command}`);\n }),\n });\n\n const report = await getAgentReadinessReport(\"copilot\", options);\n\n expect(report.auth).toMatchObject({\n state: \"authenticated\",\n source: \"gh auth status --hostname github.com\",\n provider: \"github\",\n availableProviders: [\"GitHub\"],\n status: \"pass\",\n });\n });\n});\n"],"names":["path","describe","expect","it","vi","getAgentReadinessReport","getAgentReadinessReports","HARNESS_RUNTIME_PROFILES","builtInSkillNames","skillRoots","claude","codex","copilot","gemini_cli","grok_cli","antigravity_cli","opencode","pi","fixture","overrides","homeDir","assetRoot","files","join","JSON","stringify","hooks","SessionStart","type","command","PreToolUse","anthropic","access","refresh","litellm","key","env","LITELLM_BASE_URL","directory","root","Object","values","skill","executablePaths","map","agent","options","readFile","target","Error","includes","runCommand","fn","stdout","stderr","loggedIn","codexAuth","reports","keys","toEqual","report","entries","toBe","executable","status","globalConfig","builtInSkills","toMatchObject","present","required","kiro","missing","integration","label","installed","toBeUndefined","spacedBin","codexPath","delimiter","startsWith","serialized","auth","state","provider","availableProviders","not","toContain","source"],"mappings":"AAAA,OAAOA,UAAU,YAAY;AAC7B,SAASC,QAAQ,EAAEC,MAAM,EAAEC,EAAE,EAAEC,EAAE,QAAQ,SAAS;AAClD,SACEC,uBAAuB,EACvBC,wBAAwB,QAGnB,iDAAiD;AACxD,SAASC,wBAAwB,QAAQ,wCAAwC;AAIjF,MAAMC,oBAAoB;IAAC;IAAoB;IAAU;CAAS;AAClE,MAAMC,aAAqC;IACzCC,QAAQ;IACRC,OAAO;IACPC,SAAS;IACTC,YAAY;IACZC,UAAU;IACVC,iBAAiB;IACjBC,UAAU;IACVC,IAAI;AACN;AAEA,SAASC,QAAQC,YAA4C,CAAC,CAAC;IAC7D,MAAMC,UAAU;IAChB,MAAMC,YAAY;IAClB,MAAMC,QAAe;QACnB,CAACtB,KAAKuB,IAAI,CAACH,SAAS,UAAU,SAAS,6BAA6B,EAAE;QACtE,CAACpB,KAAKuB,IAAI,CAACF,WAAW,SAAS,6BAA6B,EAAE;QAC9D,CAACrB,KAAKuB,IAAI,CAACH,SAAS,UAAU,cAAc,EAAEI,KAAKC,SAAS,CAAC;YAC3DC,OAAO;gBACLC,cAAc;oBACZ;wBACED,OAAO;4BAAC;gCAAEE,MAAM;gCAAWC,SAAS;4BAAgD;yBAAE;oBACxF;iBACD;YACH;QACF;QACA,CAAC7B,KAAKuB,IAAI,CAACH,SAAS,UAAU,aAAa,iBAAiB,EAAEI,KAAKC,SAAS,CAAC;YAC3E,OAAO;QACT;QACA,yBAAyB;QACzB,CAACzB,KAAKuB,IAAI,CAACH,SAAS,WAAW,SAAS,yBAAyB,EAAE;QACnE,CAACpB,KAAKuB,IAAI,CAACF,WAAW,UAAU,yBAAyB,EAAE;QAC3D,CAACrB,KAAKuB,IAAI,CAACH,SAAS,WAAW,iBAAiB,EAAEI,KAAKC,SAAS,CAAC;YAC/DC,OAAO;gBACLI,YAAY;oBACV;wBAAEJ,OAAO;4BAAC;gCAAEE,MAAM;gCAAWC,SAAS;4BAA6C;yBAAE;oBAAC;iBACvF;YACH;QACF;QACA,CAAC7B,KAAKuB,IAAI,CAACH,SAAS,OAAO,SAAS,iBAAiB,EAAEI,KAAKC,SAAS,CAAC;YACpE,OAAO;QACT;QACA,CAACzB,KAAKuB,IAAI,CAACH,SAAS,OAAO,SAAS,aAAa,EAAEI,KAAKC,SAAS,CAAC;YAChEM,WAAW;gBAAEC,QAAQ;gBAAiBC,SAAS;YAAiB;YAChEC,SAAS;gBAAEC,KAAK;gBAAkBC,KAAK;oBAAEC,kBAAkB;gBAA+B;YAAE;QAC9F;IACF;IACA,KAAK,MAAMC,aAAa;QACtB;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;KACD,CAAE;QACDhB,KAAK,CAACtB,KAAKuB,IAAI,CAACH,SAASkB,WAAW,GAAG;IACzC;IACA,KAAK,MAAMC,QAAQC,OAAOC,MAAM,CAAChC,YAAa;QAC5C,KAAK,MAAMiC,SAASlC,kBAAmB;YACrCc,KAAK,CAACtB,KAAKuB,IAAI,CAACH,SAASmB,MAAMG,OAAO,YAAY,GAAG;QACvD;IACF;IACA,MAAMC,kBAAkBH,OAAOC,MAAM,CAAClC,0BAA0BqC,GAAG,CAAC,CAACC,QACnE7C,KAAKuB,IAAI,CAAC,QAAQsB,MAAMhB,OAAO;IAEjC,MAAMiB,UAAiC;QACrC1B;QACApB,MAAM;QACNqB;QACAb;QACAC;QACAsC,UAAU,OAAOC;YACf,IAAI,CAAEA,CAAAA,UAAU1B,KAAI,GAAI,MAAM,IAAI2B,MAAM;YACxC,OAAO3B,KAAK,CAAC0B,OAAO;QACtB;QACAhB,QAAQ,OAAOgB;YACb,IAAIL,gBAAgBO,QAAQ,CAACF,WAAWA,UAAU1B,OAAO;YACzD,MAAM,IAAI2B,MAAM;QAClB;QACAE,YAAY/C,GAAGgD,EAAE,CAAC,OAAOvB;YACvB,IAAIA,YAAY,MAAM,OAAO;gBAAEwB,QAAQ;gBAAuCC,QAAQ;YAAG;YACzF,IAAIzB,YAAY,UAAU,OAAO;gBAAEwB,QAAQ7B,KAAKC,SAAS,CAAC;oBAAE8B,UAAU;gBAAK;gBAAID,QAAQ;YAAG;YAC1F,IAAIzB,YAAY,MACd,OAAO;gBAAEwB,QAAQ;gBAA6DC,QAAQ;YAAG;YAC3F,IAAIzB,YAAY,YACd,OAAO;gBAAEwB,QAAQ;gBAAqCC,QAAQ;YAAG;YACnE,MAAM,IAAIL,MAAM,CAAC,mBAAmB,EAAEpB,SAAS;QACjD;QACA2B,WAAW,UAAY;QACvB,GAAGrC,SAAS;IACd;IACA,OAAO;QAAE2B;QAASxB;IAAM;AAC1B;AAEArB,SAAS,mBAAmB;IAC1BE,GAAG,4FAA4F;QAC7F,MAAM,EAAE2C,OAAO,EAAE,GAAG5B;QAEpB,MAAMuC,UAAU,MAAMnD,yBAAyBwC;QAE/C5C,OAAOsC,OAAOkB,IAAI,CAACD,UAAUE,OAAO,CAAC;YACnC;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;SACD;QACD,KAAK,MAAM,CAACd,OAAOe,OAAO,IAAIpB,OAAOqB,OAAO,CAACJ,SAE1C;YACDvD,OAAO0D,OAAOhC,IAAI,EAAEkC,IAAI,CAACjB;YACzB3C,OAAO0D,OAAOG,UAAU,CAACC,MAAM,EAAEF,IAAI,CAAC;YACtC5D,OAAO0D,OAAOK,YAAY,CAACD,MAAM,EAAEF,IAAI,CAAC;YACxC5D,OAAO0D,OAAOM,aAAa,CAACF,MAAM,EAAEF,IAAI,CAAC;QAC3C;QACA5D,OAAOuD,QAAQ5C,UAAU,CAACqD,aAAa,EAAEC,aAAa,CAAC;YAAEC,SAAS;YAAGC,UAAU;QAAE;QACjFnE,OAAOuD,QAAQ1C,eAAe,CAACmD,aAAa,EAAEC,aAAa,CAAC;YAAEC,SAAS;YAAGC,UAAU;QAAE;QACtFnE,OAAOuD,QAAQa,IAAI,CAACJ,aAAa,EAAEC,aAAa,CAAC;YAC/CnE,MAAM;YACNoE,SAAS;YACTC,UAAU;YACVE,SAAS/D;QACX;QACAN,OAAOuD,QAAQ9C,KAAK,CAAC6D,WAAW,EAAEL,aAAa,CAAC;YAC9CM,OAAO;YACPC,WAAW;YACXV,QAAQ;QACV;QACA9D,OAAOuD,QAAQ/C,MAAM,CAAC8D,WAAW,EAAEL,aAAa,CAAC;YAC/CM,OAAO;YACPC,WAAW;YACXV,QAAQ;QACV;QACA9D,OAAOuD,QAAQxC,EAAE,CAACuD,WAAW,EAAEL,aAAa,CAAC;YAC3CM,OAAO;YACPC,WAAW;YACXV,QAAQ;QACV;QACA9D,OAAOuD,QAAQ7C,OAAO,CAAC4D,WAAW,EAAEG,aAAa;QACjDzE,OAAOuD,QAAQ5C,UAAU,CAAC2D,WAAW,EAAEG,aAAa;QACpDzE,OAAOuD,QAAQ3C,QAAQ,CAAC0D,WAAW,EAAEG,aAAa;QAClDzE,OAAOuD,QAAQa,IAAI,CAACE,WAAW,EAAEG,aAAa;QAC9CzE,OAAOuD,QAAQ1C,eAAe,CAACyD,WAAW,EAAEG,aAAa;QACzDzE,OAAOuD,QAAQzC,QAAQ,CAACwD,WAAW,EAAEG,aAAa;IACpD;IAEAxE,GAAG,sEAAsE;QACvE,MAAMyE,YAAY;QAClB,MAAMC,YAAY7E,KAAKuB,IAAI,CAACqD,WAAWrE,yBAAyBI,KAAK,CAACkB,OAAO;QAC7E,MAAM,EAAEiB,OAAO,EAAE,GAAG5B,QAAQ;YAC1BlB,MAAM;gBAAC4E;gBAAW;aAAiB,CAACrD,IAAI,CAACvB,KAAK8E,SAAS;YACvD9C,QAAQ,OAAOgB;gBACb,IAAIA,WAAW6B,aAAa7B,OAAO+B,UAAU,CAAC,eAAe;gBAC7D,MAAM,IAAI9B,MAAM;YAClB;QACF;QAEA,MAAMW,SAAS,MAAMvD,wBAAwB,SAASyC;QAEtD5C,OAAO0D,OAAOG,UAAU,EAAEI,aAAa,CAAC;YAAEtC,SAAS;YAAS7B,MAAM6E;YAAWb,QAAQ;QAAO;IAC9F;IAEA7D,GAAG,gEAAgE;QACjE,MAAM,EAAE2C,OAAO,EAAE,GAAG5B;QAEpB,MAAM0C,SAAS,MAAMvD,wBAAwB,MAAMyC;QACnD,MAAMkC,aAAaxD,KAAKC,SAAS,CAACmC;QAElC1D,OAAO0D,OAAOqB,IAAI,EAAEd,aAAa,CAAC;YAChCe,OAAO;YACPC,UAAU;YACVC,oBAAoB;gBAAC;gBAAa;aAAU;YAC5CpB,QAAQ;QACV;QACA9D,OAAO8E,YAAYK,GAAG,CAACC,SAAS,CAAC;QACjCpF,OAAO8E,YAAYK,GAAG,CAACC,SAAS,CAAC;QACjCpF,OAAO8E,YAAYK,GAAG,CAACC,SAAS,CAAC;QACjCpF,OAAO8E,YAAYK,GAAG,CAACC,SAAS,CAAC;IACnC;IAEAnF,GAAG,gEAAgE;QACjE,MAAM,EAAE2C,OAAO,EAAE,GAAG5B,QAAQ;YAC1BiC,YAAY/C,GAAGgD,EAAE,CAAC,OAAOvB;gBACvB,IAAIA,YAAY,MAAM,OAAO;oBAAEwB,QAAQ;oBAAqBC,QAAQ;gBAAG;gBACvE,IAAIzB,YAAY,UAAU,OAAO;oBAAEwB,QAAQ7B,KAAKC,SAAS,CAAC;wBAAE8B,UAAU;oBAAK;oBAAID,QAAQ;gBAAG;gBAC1F,MAAM,IAAIL,MAAM,CAAC,mBAAmB,EAAEpB,SAAS;YACjD;QACF;QAEA,MAAM+B,SAAS,MAAMvD,wBAAwB,MAAMyC;QAEnD5C,OAAO0D,OAAOY,WAAW,EAAEL,aAAa,CAAC;YACvCM,OAAO;YACPC,WAAW;YACXV,QAAQ;QACV;IACF;IAEA7D,GAAG,8DAA8D;QAC/D,MAAM,EAAE2C,OAAO,EAAExB,KAAK,EAAE,GAAGJ;QAC3B,OAAOI,KAAK,CAACtB,KAAKuB,IAAI,CAACuB,QAAQ1B,OAAO,EAAGX,WAAWE,KAAK,EAAE,UAAU,YAAY;QAEjF,MAAMiD,SAAS,MAAMvD,wBAAwB,SAASyC;QAEtD5C,OAAO0D,OAAOM,aAAa,EAAEC,aAAa,CAAC;YACzCH,QAAQ;YACRI,SAAS;YACTC,UAAU;YACVE,SAAS;gBAAC;aAAS;QACrB;QACArE,OAAO0D,OAAOI,MAAM,EAAEF,IAAI,CAAC;IAC7B;IAEA3D,GAAG,8CAA8C;QAC/C,MAAM,EAAE2C,OAAO,EAAE,GAAG5B,QAAQ;YAC1BiC,YAAY/C,GAAGgD,EAAE,CAAC,OAAOvB;gBACvB,IAAIA,YAAY,YAAY;oBAC1B,OAAO;wBACLwB,QAAQ;wBACRC,QAAQ;oBACV;gBACF;gBACA,IAAIzB,YAAY,MAAM,OAAO;oBAAEwB,QAAQ;oBAAuCC,QAAQ;gBAAG;gBACzF,IAAIzB,YAAY,UAAU,OAAO;oBAAEwB,QAAQ7B,KAAKC,SAAS,CAAC;wBAAE8B,UAAU;oBAAK;oBAAID,QAAQ;gBAAG;gBAC1F,MAAM,IAAIL,MAAM,CAAC,mBAAmB,EAAEpB,SAAS;YACjD;QACF;QAEA,MAAM+B,SAAS,MAAMvD,wBAAwB,YAAYyC;QAEzD5C,OAAO0D,OAAOqB,IAAI,EAAEd,aAAa,CAAC;YAChCe,OAAO;YACPK,QAAQ;YACRJ,UAAU;YACVC,oBAAoB;gBAAC;gBAAc;gBAAU;aAAU;YACvDpB,QAAQ;QACV;IACF;IAEA7D,GAAG,yDAAyD;QAC1D,MAAM,EAAE2C,OAAO,EAAE,GAAG5B,QAAQ;YAC1BiC,YAAY/C,GAAGgD,EAAE,CAAC,OAAOvB;gBACvB,IAAIA,YAAY,MACd,OAAO;oBACLwB,QAAQ;oBACRC,QAAQ;gBACV;gBACF,IAAIzB,YAAY,MAAM,OAAO;oBAAEwB,QAAQ;oBAAuCC,QAAQ;gBAAG;gBACzF,IAAIzB,YAAY,UAAU,OAAO;oBAAEwB,QAAQ7B,KAAKC,SAAS,CAAC;wBAAE8B,UAAU;oBAAK;oBAAID,QAAQ;gBAAG;gBAC1F,IAAIzB,YAAY,YAAY,OAAO;oBAAEwB,QAAQ;oBAAoBC,QAAQ;gBAAG;gBAC5E,MAAM,IAAIL,MAAM,CAAC,mBAAmB,EAAEpB,SAAS;YACjD;QACF;QAEA,MAAM+B,SAAS,MAAMvD,wBAAwB,WAAWyC;QAExD5C,OAAO0D,OAAOqB,IAAI,EAAEd,aAAa,CAAC;YAChCe,OAAO;YACPK,QAAQ;YACRJ,UAAU;YACVC,oBAAoB;gBAAC;aAAS;YAC9BpB,QAAQ;QACV;IACF;AACF"}
|
|
1
|
+
{"version":3,"sources":["../../../../src/__tests__/harnesses/readiness/AgentReadiness.test.ts"],"sourcesContent":["import path from \"node:path\";\nimport { describe, expect, it, vi } from \"vitest\";\nimport {\n getAgentReadinessReport,\n getAgentReadinessReports,\n type AgentReadinessOptions,\n type ReadinessAgentType,\n} from \"../../../harnesses/readiness/AgentReadiness.js\";\nimport { HARNESS_RUNTIME_PROFILES } from \"../../../harnesses/runtimeProfiles.js\";\n\ntype Files = Record<string, string>;\n\nconst builtInSkillNames = [\"agent-management\", \"memory\", \"verify\"];\nconst skillRoots: Record<string, string> = {\n claude: \".claude/skills\",\n codex: \".codex/skills\",\n copilot: \".copilot/skills\",\n gemini_cli: \".gemini/skills\",\n grok_cli: \".grok/skills\",\n antigravity_cli: \".gemini/config/skills\",\n opencode: \".config/opencode/skill\",\n pi: \".pi/agent/skills\",\n};\n\nfunction fixture(overrides: Partial<AgentReadinessOptions> = {}) {\n const homeDir = \"/home/test\";\n const assetRoot = \"/assets\";\n const files: Files = {\n [path.join(homeDir, \".codex\", \"hooks\", \"codex-session-mapping.cjs\")]: \"codex-hook\",\n [path.join(assetRoot, \"codex\", \"codex-session-mapping.cjs\")]: \"codex-hook\",\n [path.join(homeDir, \".codex\", \"hooks.json\")]: JSON.stringify({\n hooks: {\n SessionStart: [\n {\n hooks: [{ type: \"command\", command: \"node ~/.codex/hooks/codex-session-mapping.cjs\" }],\n },\n ],\n },\n }),\n [path.join(homeDir, \".codex\", \"ai-devkit\", \"sessions.json\")]: JSON.stringify({\n \"123\": \"/sessions/codex.jsonl\",\n }),\n \"/sessions/codex.jsonl\": \"\",\n [path.join(homeDir, \".claude\", \"hooks\", \"claude-prompt-hook.js\")]: \"claude-hook\",\n [path.join(assetRoot, \"claude\", \"claude-prompt-hook.js\")]: \"claude-hook\",\n [path.join(homeDir, \".claude\", \"settings.json\")]: JSON.stringify({\n hooks: {\n PreToolUse: [\n { hooks: [{ type: \"command\", command: \"node ~/.claude/hooks/claude-prompt-hook.js\" }] },\n ],\n },\n }),\n [path.join(homeDir, \".pi\", \"agent\", \"sessions.json\")]: JSON.stringify({\n \"456\": \"/sessions/missing-pi.jsonl\",\n }),\n [path.join(homeDir, \".pi\", \"agent\", \"auth.json\")]: JSON.stringify({\n anthropic: { access: \"access-secret\", refresh: \"refresh-secret\" },\n litellm: { key: \"litellm-secret\", env: { LITELLM_BASE_URL: \"https://litellm.example.test\" } },\n }),\n };\n for (const directory of [\n \".claude\",\n \".codex\",\n \".copilot\",\n \".gemini\",\n \".grok\",\n \".kiro\",\n \".gemini/antigravity-cli\",\n \".config/opencode\",\n \".config/devin\",\n \".pi\",\n ]) {\n files[path.join(homeDir, directory)] = \"<dir>\";\n }\n for (const root of Object.values(skillRoots)) {\n for (const skill of builtInSkillNames) {\n files[path.join(homeDir, root, skill, \"SKILL.md\")] = \"# skill\";\n }\n }\n const executablePaths = Object.values(HARNESS_RUNTIME_PROFILES).map((agent) =>\n path.join(\"/bin\", agent.command),\n );\n const options: AgentReadinessOptions = {\n homeDir,\n path: \"/bin\",\n assetRoot,\n builtInSkillNames,\n skillRoots,\n readFile: async (target) => {\n if (!(target in files)) throw new Error(\"missing\");\n return files[target];\n },\n access: async (target) => {\n if (executablePaths.includes(target) || target in files) return;\n throw new Error(\"missing\");\n },\n runCommand: vi.fn(async (command) => {\n if (command === \"pi\") return { stdout: \"npm:@ai-devkit/pi-session-tracker\\n\", stderr: \"\" };\n if (command === \"claude\") return { stdout: JSON.stringify({ loggedIn: true }), stderr: \"\" };\n if (command === \"gh\")\n return { stdout: \"github.com\\n Logged in to github.com account test-user\\n\", stderr: \"\" };\n if (command === \"opencode\")\n return { stdout: \"● litellm api\\n● OpenAI oauth\\n\", stderr: \"\" };\n if (command === \"devin\")\n return { stdout: \"Logged in (via Devin).\\n\\nUser:\\n Name: test-user\\n\", stderr: \"\" };\n throw new Error(`unexpected command ${command}`);\n }),\n codexAuth: async () => true,\n ...overrides,\n };\n return { options, files };\n}\n\ndescribe(\"agent readiness\", () => {\n it(\"reports every readiness-supported adapter with generic checks and only real integrations\", async () => {\n const { options } = fixture();\n\n const reports = await getAgentReadinessReports(options);\n\n expect(Object.keys(reports)).toEqual([\n \"claude\",\n \"codex\",\n \"gemini_cli\",\n \"grok_cli\",\n \"kiro\",\n \"antigravity_cli\",\n \"opencode\",\n \"copilot\",\n \"pi\",\n \"devin\",\n ]);\n for (const [agent, report] of Object.entries(reports) as Array<\n [ReadinessAgentType, (typeof reports)[ReadinessAgentType]]\n >) {\n expect(report.type).toBe(agent);\n expect(report.executable.status).toBe(\"pass\");\n expect(report.globalConfig.status).toBe(\"pass\");\n expect(report.builtInSkills.status).toBe(\"info\");\n }\n expect(reports.gemini_cli.builtInSkills).toMatchObject({ present: 3, required: 3 });\n expect(reports.antigravity_cli.builtInSkills).toMatchObject({ present: 3, required: 3 });\n expect(reports.kiro.builtInSkills).toMatchObject({\n path: null,\n present: 0,\n required: 3,\n missing: builtInSkillNames,\n });\n expect(reports.codex.integration).toMatchObject({\n label: \"ai-devkit hook\",\n installed: true,\n status: \"pass\",\n });\n expect(reports.claude.integration).toMatchObject({\n label: \"ai-devkit hook\",\n installed: true,\n status: \"pass\",\n });\n expect(reports.pi.integration).toMatchObject({\n label: \"ai-devkit plugin\",\n installed: true,\n status: \"pass\",\n });\n expect(reports.copilot.integration).toBeUndefined();\n expect(reports.gemini_cli.integration).toBeUndefined();\n expect(reports.grok_cli.integration).toBeUndefined();\n expect(reports.kiro.integration).toBeUndefined();\n expect(reports.antigravity_cli.integration).toBeUndefined();\n expect(reports.opencode.integration).toBeUndefined();\n });\n\n it(\"resolves agent executables from PATH directories containing spaces\", async () => {\n const spacedBin = \"/Applications/Some App.app/Contents/Resources\";\n const codexPath = path.join(spacedBin, HARNESS_RUNTIME_PROFILES.codex.command);\n const { options } = fixture({\n path: [spacedBin, \"/usr/local/bin\"].join(path.delimiter),\n access: async (target) => {\n if (target === codexPath || target.startsWith(\"/home/test\")) return;\n throw new Error(\"missing\");\n },\n });\n\n const report = await getAgentReadinessReport(\"codex\", options);\n\n expect(report.executable).toMatchObject({ command: \"codex\", path: codexPath, status: \"pass\" });\n });\n\n it(\"parses Pi provider names without returning credential values\", async () => {\n const { options } = fixture();\n\n const report = await getAgentReadinessReport(\"pi\", options);\n const serialized = JSON.stringify(report);\n\n expect(report.auth).toMatchObject({\n state: \"authenticated\",\n provider: null,\n availableProviders: [\"anthropic\", \"litellm\"],\n status: \"pass\",\n });\n expect(serialized).not.toContain(\"access-secret\");\n expect(serialized).not.toContain(\"refresh-secret\");\n expect(serialized).not.toContain(\"litellm-secret\");\n expect(serialized).not.toContain(\"litellm.example.test\");\n });\n\n it(\"accepts Pi session tracker display names from pi list output\", async () => {\n const { options } = fixture({\n runCommand: vi.fn(async (command) => {\n if (command === \"pi\") return { stdout: \"session tracker\\n\", stderr: \"\" };\n if (command === \"claude\") return { stdout: JSON.stringify({ loggedIn: true }), stderr: \"\" };\n throw new Error(`unexpected command ${command}`);\n }),\n });\n\n const report = await getAgentReadinessReport(\"pi\", options);\n\n expect(report.integration).toMatchObject({\n label: \"ai-devkit plugin\",\n installed: true,\n status: \"pass\",\n });\n });\n\n it(\"reports missing built-in skills as informational readiness\", async () => {\n const { options, files } = fixture();\n delete files[path.join(options.homeDir!, skillRoots.codex, \"verify\", \"SKILL.md\")];\n\n const report = await getAgentReadinessReport(\"codex\", options);\n\n expect(report.builtInSkills).toMatchObject({\n status: \"info\",\n present: 2,\n required: 3,\n missing: [\"verify\"],\n });\n expect(report.status).toBe(\"pass\");\n });\n\n it(\"checks OpenCode auth from auth list output\", async () => {\n const { options } = fixture({\n runCommand: vi.fn(async (command) => {\n if (command === \"opencode\") {\n return {\n stdout: \"\\x1B[32m●\\x1B[0m litellm api\\n● OpenAI oauth\\n● GitLab Duo GITLAB_TOKEN\\n\",\n stderr: \"\",\n };\n }\n if (command === \"pi\") return { stdout: \"npm:@ai-devkit/pi-session-tracker\\n\", stderr: \"\" };\n if (command === \"claude\") return { stdout: JSON.stringify({ loggedIn: true }), stderr: \"\" };\n throw new Error(`unexpected command ${command}`);\n }),\n });\n\n const report = await getAgentReadinessReport(\"opencode\", options);\n\n expect(report.auth).toMatchObject({\n state: \"authenticated\",\n source: \"opencode auth list\",\n provider: null,\n availableProviders: [\"GitLab Duo\", \"OpenAI\", \"litellm\"],\n status: \"pass\",\n });\n });\n\n it(\"checks Copilot auth through GitHub CLI authentication\", async () => {\n const { options } = fixture({\n runCommand: vi.fn(async (command) => {\n if (command === \"gh\")\n return {\n stdout: \"github.com\\n Logged in to github.com account test-user\\n\",\n stderr: \"\",\n };\n if (command === \"pi\") return { stdout: \"npm:@ai-devkit/pi-session-tracker\\n\", stderr: \"\" };\n if (command === \"claude\") return { stdout: JSON.stringify({ loggedIn: true }), stderr: \"\" };\n if (command === \"opencode\") return { stdout: \"● litellm api\\n\", stderr: \"\" };\n throw new Error(`unexpected command ${command}`);\n }),\n });\n\n const report = await getAgentReadinessReport(\"copilot\", options);\n\n expect(report.auth).toMatchObject({\n state: \"authenticated\",\n source: \"gh auth status --hostname github.com\",\n provider: \"github\",\n availableProviders: [\"GitHub\"],\n status: \"pass\",\n });\n });\n\n it(\"checks Devin auth from devin auth status output\", async () => {\n const { options } = fixture({\n runCommand: vi.fn(async (command) => {\n if (command === \"devin\")\n return { stdout: \"Logged in (via Devin).\\n\\nUser:\\n Name: test-user\\n\", stderr: \"\" };\n if (command === \"pi\") return { stdout: \"npm:@ai-devkit/pi-session-tracker\\n\", stderr: \"\" };\n if (command === \"claude\") return { stdout: JSON.stringify({ loggedIn: true }), stderr: \"\" };\n throw new Error(`unexpected command ${command}`);\n }),\n });\n\n const report = await getAgentReadinessReport(\"devin\", options);\n\n expect(report.auth).toMatchObject({\n state: \"authenticated\",\n source: \"devin auth status\",\n provider: null,\n availableProviders: [\"devin\"],\n status: \"pass\",\n });\n });\n\n it(\"reports Devin as unauthenticated without a login line\", async () => {\n const { options } = fixture({\n runCommand: vi.fn(async (command) => {\n if (command === \"devin\") return { stdout: \"Not logged in\\n\", stderr: \"\" };\n if (command === \"pi\") return { stdout: \"npm:@ai-devkit/pi-session-tracker\\n\", stderr: \"\" };\n if (command === \"claude\") return { stdout: JSON.stringify({ loggedIn: true }), stderr: \"\" };\n throw new Error(`unexpected command ${command}`);\n }),\n });\n\n const report = await getAgentReadinessReport(\"devin\", options);\n\n expect(report.auth).toMatchObject({ state: \"unauthenticated\", status: \"fail\" });\n });\n});\n"],"names":["path","describe","expect","it","vi","getAgentReadinessReport","getAgentReadinessReports","HARNESS_RUNTIME_PROFILES","builtInSkillNames","skillRoots","claude","codex","copilot","gemini_cli","grok_cli","antigravity_cli","opencode","pi","fixture","overrides","homeDir","assetRoot","files","join","JSON","stringify","hooks","SessionStart","type","command","PreToolUse","anthropic","access","refresh","litellm","key","env","LITELLM_BASE_URL","directory","root","Object","values","skill","executablePaths","map","agent","options","readFile","target","Error","includes","runCommand","fn","stdout","stderr","loggedIn","codexAuth","reports","keys","toEqual","report","entries","toBe","executable","status","globalConfig","builtInSkills","toMatchObject","present","required","kiro","missing","integration","label","installed","toBeUndefined","spacedBin","codexPath","delimiter","startsWith","serialized","auth","state","provider","availableProviders","not","toContain","source"],"mappings":"AAAA,OAAOA,UAAU,YAAY;AAC7B,SAASC,QAAQ,EAAEC,MAAM,EAAEC,EAAE,EAAEC,EAAE,QAAQ,SAAS;AAClD,SACEC,uBAAuB,EACvBC,wBAAwB,QAGnB,iDAAiD;AACxD,SAASC,wBAAwB,QAAQ,wCAAwC;AAIjF,MAAMC,oBAAoB;IAAC;IAAoB;IAAU;CAAS;AAClE,MAAMC,aAAqC;IACzCC,QAAQ;IACRC,OAAO;IACPC,SAAS;IACTC,YAAY;IACZC,UAAU;IACVC,iBAAiB;IACjBC,UAAU;IACVC,IAAI;AACN;AAEA,SAASC,QAAQC,YAA4C,CAAC,CAAC;IAC7D,MAAMC,UAAU;IAChB,MAAMC,YAAY;IAClB,MAAMC,QAAe;QACnB,CAACtB,KAAKuB,IAAI,CAACH,SAAS,UAAU,SAAS,6BAA6B,EAAE;QACtE,CAACpB,KAAKuB,IAAI,CAACF,WAAW,SAAS,6BAA6B,EAAE;QAC9D,CAACrB,KAAKuB,IAAI,CAACH,SAAS,UAAU,cAAc,EAAEI,KAAKC,SAAS,CAAC;YAC3DC,OAAO;gBACLC,cAAc;oBACZ;wBACED,OAAO;4BAAC;gCAAEE,MAAM;gCAAWC,SAAS;4BAAgD;yBAAE;oBACxF;iBACD;YACH;QACF;QACA,CAAC7B,KAAKuB,IAAI,CAACH,SAAS,UAAU,aAAa,iBAAiB,EAAEI,KAAKC,SAAS,CAAC;YAC3E,OAAO;QACT;QACA,yBAAyB;QACzB,CAACzB,KAAKuB,IAAI,CAACH,SAAS,WAAW,SAAS,yBAAyB,EAAE;QACnE,CAACpB,KAAKuB,IAAI,CAACF,WAAW,UAAU,yBAAyB,EAAE;QAC3D,CAACrB,KAAKuB,IAAI,CAACH,SAAS,WAAW,iBAAiB,EAAEI,KAAKC,SAAS,CAAC;YAC/DC,OAAO;gBACLI,YAAY;oBACV;wBAAEJ,OAAO;4BAAC;gCAAEE,MAAM;gCAAWC,SAAS;4BAA6C;yBAAE;oBAAC;iBACvF;YACH;QACF;QACA,CAAC7B,KAAKuB,IAAI,CAACH,SAAS,OAAO,SAAS,iBAAiB,EAAEI,KAAKC,SAAS,CAAC;YACpE,OAAO;QACT;QACA,CAACzB,KAAKuB,IAAI,CAACH,SAAS,OAAO,SAAS,aAAa,EAAEI,KAAKC,SAAS,CAAC;YAChEM,WAAW;gBAAEC,QAAQ;gBAAiBC,SAAS;YAAiB;YAChEC,SAAS;gBAAEC,KAAK;gBAAkBC,KAAK;oBAAEC,kBAAkB;gBAA+B;YAAE;QAC9F;IACF;IACA,KAAK,MAAMC,aAAa;QACtB;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;QACA;KACD,CAAE;QACDhB,KAAK,CAACtB,KAAKuB,IAAI,CAACH,SAASkB,WAAW,GAAG;IACzC;IACA,KAAK,MAAMC,QAAQC,OAAOC,MAAM,CAAChC,YAAa;QAC5C,KAAK,MAAMiC,SAASlC,kBAAmB;YACrCc,KAAK,CAACtB,KAAKuB,IAAI,CAACH,SAASmB,MAAMG,OAAO,YAAY,GAAG;QACvD;IACF;IACA,MAAMC,kBAAkBH,OAAOC,MAAM,CAAClC,0BAA0BqC,GAAG,CAAC,CAACC,QACnE7C,KAAKuB,IAAI,CAAC,QAAQsB,MAAMhB,OAAO;IAEjC,MAAMiB,UAAiC;QACrC1B;QACApB,MAAM;QACNqB;QACAb;QACAC;QACAsC,UAAU,OAAOC;YACf,IAAI,CAAEA,CAAAA,UAAU1B,KAAI,GAAI,MAAM,IAAI2B,MAAM;YACxC,OAAO3B,KAAK,CAAC0B,OAAO;QACtB;QACAhB,QAAQ,OAAOgB;YACb,IAAIL,gBAAgBO,QAAQ,CAACF,WAAWA,UAAU1B,OAAO;YACzD,MAAM,IAAI2B,MAAM;QAClB;QACAE,YAAY/C,GAAGgD,EAAE,CAAC,OAAOvB;YACvB,IAAIA,YAAY,MAAM,OAAO;gBAAEwB,QAAQ;gBAAuCC,QAAQ;YAAG;YACzF,IAAIzB,YAAY,UAAU,OAAO;gBAAEwB,QAAQ7B,KAAKC,SAAS,CAAC;oBAAE8B,UAAU;gBAAK;gBAAID,QAAQ;YAAG;YAC1F,IAAIzB,YAAY,MACd,OAAO;gBAAEwB,QAAQ;gBAA6DC,QAAQ;YAAG;YAC3F,IAAIzB,YAAY,YACd,OAAO;gBAAEwB,QAAQ;gBAAqCC,QAAQ;YAAG;YACnE,IAAIzB,YAAY,SACd,OAAO;gBAAEwB,QAAQ;gBAAwDC,QAAQ;YAAG;YACtF,MAAM,IAAIL,MAAM,CAAC,mBAAmB,EAAEpB,SAAS;QACjD;QACA2B,WAAW,UAAY;QACvB,GAAGrC,SAAS;IACd;IACA,OAAO;QAAE2B;QAASxB;IAAM;AAC1B;AAEArB,SAAS,mBAAmB;IAC1BE,GAAG,4FAA4F;QAC7F,MAAM,EAAE2C,OAAO,EAAE,GAAG5B;QAEpB,MAAMuC,UAAU,MAAMnD,yBAAyBwC;QAE/C5C,OAAOsC,OAAOkB,IAAI,CAACD,UAAUE,OAAO,CAAC;YACnC;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;YACA;SACD;QACD,KAAK,MAAM,CAACd,OAAOe,OAAO,IAAIpB,OAAOqB,OAAO,CAACJ,SAE1C;YACDvD,OAAO0D,OAAOhC,IAAI,EAAEkC,IAAI,CAACjB;YACzB3C,OAAO0D,OAAOG,UAAU,CAACC,MAAM,EAAEF,IAAI,CAAC;YACtC5D,OAAO0D,OAAOK,YAAY,CAACD,MAAM,EAAEF,IAAI,CAAC;YACxC5D,OAAO0D,OAAOM,aAAa,CAACF,MAAM,EAAEF,IAAI,CAAC;QAC3C;QACA5D,OAAOuD,QAAQ5C,UAAU,CAACqD,aAAa,EAAEC,aAAa,CAAC;YAAEC,SAAS;YAAGC,UAAU;QAAE;QACjFnE,OAAOuD,QAAQ1C,eAAe,CAACmD,aAAa,EAAEC,aAAa,CAAC;YAAEC,SAAS;YAAGC,UAAU;QAAE;QACtFnE,OAAOuD,QAAQa,IAAI,CAACJ,aAAa,EAAEC,aAAa,CAAC;YAC/CnE,MAAM;YACNoE,SAAS;YACTC,UAAU;YACVE,SAAS/D;QACX;QACAN,OAAOuD,QAAQ9C,KAAK,CAAC6D,WAAW,EAAEL,aAAa,CAAC;YAC9CM,OAAO;YACPC,WAAW;YACXV,QAAQ;QACV;QACA9D,OAAOuD,QAAQ/C,MAAM,CAAC8D,WAAW,EAAEL,aAAa,CAAC;YAC/CM,OAAO;YACPC,WAAW;YACXV,QAAQ;QACV;QACA9D,OAAOuD,QAAQxC,EAAE,CAACuD,WAAW,EAAEL,aAAa,CAAC;YAC3CM,OAAO;YACPC,WAAW;YACXV,QAAQ;QACV;QACA9D,OAAOuD,QAAQ7C,OAAO,CAAC4D,WAAW,EAAEG,aAAa;QACjDzE,OAAOuD,QAAQ5C,UAAU,CAAC2D,WAAW,EAAEG,aAAa;QACpDzE,OAAOuD,QAAQ3C,QAAQ,CAAC0D,WAAW,EAAEG,aAAa;QAClDzE,OAAOuD,QAAQa,IAAI,CAACE,WAAW,EAAEG,aAAa;QAC9CzE,OAAOuD,QAAQ1C,eAAe,CAACyD,WAAW,EAAEG,aAAa;QACzDzE,OAAOuD,QAAQzC,QAAQ,CAACwD,WAAW,EAAEG,aAAa;IACpD;IAEAxE,GAAG,sEAAsE;QACvE,MAAMyE,YAAY;QAClB,MAAMC,YAAY7E,KAAKuB,IAAI,CAACqD,WAAWrE,yBAAyBI,KAAK,CAACkB,OAAO;QAC7E,MAAM,EAAEiB,OAAO,EAAE,GAAG5B,QAAQ;YAC1BlB,MAAM;gBAAC4E;gBAAW;aAAiB,CAACrD,IAAI,CAACvB,KAAK8E,SAAS;YACvD9C,QAAQ,OAAOgB;gBACb,IAAIA,WAAW6B,aAAa7B,OAAO+B,UAAU,CAAC,eAAe;gBAC7D,MAAM,IAAI9B,MAAM;YAClB;QACF;QAEA,MAAMW,SAAS,MAAMvD,wBAAwB,SAASyC;QAEtD5C,OAAO0D,OAAOG,UAAU,EAAEI,aAAa,CAAC;YAAEtC,SAAS;YAAS7B,MAAM6E;YAAWb,QAAQ;QAAO;IAC9F;IAEA7D,GAAG,gEAAgE;QACjE,MAAM,EAAE2C,OAAO,EAAE,GAAG5B;QAEpB,MAAM0C,SAAS,MAAMvD,wBAAwB,MAAMyC;QACnD,MAAMkC,aAAaxD,KAAKC,SAAS,CAACmC;QAElC1D,OAAO0D,OAAOqB,IAAI,EAAEd,aAAa,CAAC;YAChCe,OAAO;YACPC,UAAU;YACVC,oBAAoB;gBAAC;gBAAa;aAAU;YAC5CpB,QAAQ;QACV;QACA9D,OAAO8E,YAAYK,GAAG,CAACC,SAAS,CAAC;QACjCpF,OAAO8E,YAAYK,GAAG,CAACC,SAAS,CAAC;QACjCpF,OAAO8E,YAAYK,GAAG,CAACC,SAAS,CAAC;QACjCpF,OAAO8E,YAAYK,GAAG,CAACC,SAAS,CAAC;IACnC;IAEAnF,GAAG,gEAAgE;QACjE,MAAM,EAAE2C,OAAO,EAAE,GAAG5B,QAAQ;YAC1BiC,YAAY/C,GAAGgD,EAAE,CAAC,OAAOvB;gBACvB,IAAIA,YAAY,MAAM,OAAO;oBAAEwB,QAAQ;oBAAqBC,QAAQ;gBAAG;gBACvE,IAAIzB,YAAY,UAAU,OAAO;oBAAEwB,QAAQ7B,KAAKC,SAAS,CAAC;wBAAE8B,UAAU;oBAAK;oBAAID,QAAQ;gBAAG;gBAC1F,MAAM,IAAIL,MAAM,CAAC,mBAAmB,EAAEpB,SAAS;YACjD;QACF;QAEA,MAAM+B,SAAS,MAAMvD,wBAAwB,MAAMyC;QAEnD5C,OAAO0D,OAAOY,WAAW,EAAEL,aAAa,CAAC;YACvCM,OAAO;YACPC,WAAW;YACXV,QAAQ;QACV;IACF;IAEA7D,GAAG,8DAA8D;QAC/D,MAAM,EAAE2C,OAAO,EAAExB,KAAK,EAAE,GAAGJ;QAC3B,OAAOI,KAAK,CAACtB,KAAKuB,IAAI,CAACuB,QAAQ1B,OAAO,EAAGX,WAAWE,KAAK,EAAE,UAAU,YAAY;QAEjF,MAAMiD,SAAS,MAAMvD,wBAAwB,SAASyC;QAEtD5C,OAAO0D,OAAOM,aAAa,EAAEC,aAAa,CAAC;YACzCH,QAAQ;YACRI,SAAS;YACTC,UAAU;YACVE,SAAS;gBAAC;aAAS;QACrB;QACArE,OAAO0D,OAAOI,MAAM,EAAEF,IAAI,CAAC;IAC7B;IAEA3D,GAAG,8CAA8C;QAC/C,MAAM,EAAE2C,OAAO,EAAE,GAAG5B,QAAQ;YAC1BiC,YAAY/C,GAAGgD,EAAE,CAAC,OAAOvB;gBACvB,IAAIA,YAAY,YAAY;oBAC1B,OAAO;wBACLwB,QAAQ;wBACRC,QAAQ;oBACV;gBACF;gBACA,IAAIzB,YAAY,MAAM,OAAO;oBAAEwB,QAAQ;oBAAuCC,QAAQ;gBAAG;gBACzF,IAAIzB,YAAY,UAAU,OAAO;oBAAEwB,QAAQ7B,KAAKC,SAAS,CAAC;wBAAE8B,UAAU;oBAAK;oBAAID,QAAQ;gBAAG;gBAC1F,MAAM,IAAIL,MAAM,CAAC,mBAAmB,EAAEpB,SAAS;YACjD;QACF;QAEA,MAAM+B,SAAS,MAAMvD,wBAAwB,YAAYyC;QAEzD5C,OAAO0D,OAAOqB,IAAI,EAAEd,aAAa,CAAC;YAChCe,OAAO;YACPK,QAAQ;YACRJ,UAAU;YACVC,oBAAoB;gBAAC;gBAAc;gBAAU;aAAU;YACvDpB,QAAQ;QACV;IACF;IAEA7D,GAAG,yDAAyD;QAC1D,MAAM,EAAE2C,OAAO,EAAE,GAAG5B,QAAQ;YAC1BiC,YAAY/C,GAAGgD,EAAE,CAAC,OAAOvB;gBACvB,IAAIA,YAAY,MACd,OAAO;oBACLwB,QAAQ;oBACRC,QAAQ;gBACV;gBACF,IAAIzB,YAAY,MAAM,OAAO;oBAAEwB,QAAQ;oBAAuCC,QAAQ;gBAAG;gBACzF,IAAIzB,YAAY,UAAU,OAAO;oBAAEwB,QAAQ7B,KAAKC,SAAS,CAAC;wBAAE8B,UAAU;oBAAK;oBAAID,QAAQ;gBAAG;gBAC1F,IAAIzB,YAAY,YAAY,OAAO;oBAAEwB,QAAQ;oBAAoBC,QAAQ;gBAAG;gBAC5E,MAAM,IAAIL,MAAM,CAAC,mBAAmB,EAAEpB,SAAS;YACjD;QACF;QAEA,MAAM+B,SAAS,MAAMvD,wBAAwB,WAAWyC;QAExD5C,OAAO0D,OAAOqB,IAAI,EAAEd,aAAa,CAAC;YAChCe,OAAO;YACPK,QAAQ;YACRJ,UAAU;YACVC,oBAAoB;gBAAC;aAAS;YAC9BpB,QAAQ;QACV;IACF;IAEA7D,GAAG,mDAAmD;QACpD,MAAM,EAAE2C,OAAO,EAAE,GAAG5B,QAAQ;YAC1BiC,YAAY/C,GAAGgD,EAAE,CAAC,OAAOvB;gBACvB,IAAIA,YAAY,SACd,OAAO;oBAAEwB,QAAQ;oBAAwDC,QAAQ;gBAAG;gBACtF,IAAIzB,YAAY,MAAM,OAAO;oBAAEwB,QAAQ;oBAAuCC,QAAQ;gBAAG;gBACzF,IAAIzB,YAAY,UAAU,OAAO;oBAAEwB,QAAQ7B,KAAKC,SAAS,CAAC;wBAAE8B,UAAU;oBAAK;oBAAID,QAAQ;gBAAG;gBAC1F,MAAM,IAAIL,MAAM,CAAC,mBAAmB,EAAEpB,SAAS;YACjD;QACF;QAEA,MAAM+B,SAAS,MAAMvD,wBAAwB,SAASyC;QAEtD5C,OAAO0D,OAAOqB,IAAI,EAAEd,aAAa,CAAC;YAChCe,OAAO;YACPK,QAAQ;YACRJ,UAAU;YACVC,oBAAoB;gBAAC;aAAQ;YAC7BpB,QAAQ;QACV;IACF;IAEA7D,GAAG,yDAAyD;QAC1D,MAAM,EAAE2C,OAAO,EAAE,GAAG5B,QAAQ;YAC1BiC,YAAY/C,GAAGgD,EAAE,CAAC,OAAOvB;gBACvB,IAAIA,YAAY,SAAS,OAAO;oBAAEwB,QAAQ;oBAAmBC,QAAQ;gBAAG;gBACxE,IAAIzB,YAAY,MAAM,OAAO;oBAAEwB,QAAQ;oBAAuCC,QAAQ;gBAAG;gBACzF,IAAIzB,YAAY,UAAU,OAAO;oBAAEwB,QAAQ7B,KAAKC,SAAS,CAAC;wBAAE8B,UAAU;oBAAK;oBAAID,QAAQ;gBAAG;gBAC1F,MAAM,IAAIL,MAAM,CAAC,mBAAmB,EAAEpB,SAAS;YACjD;QACF;QAEA,MAAM+B,SAAS,MAAMvD,wBAAwB,SAASyC;QAEtD5C,OAAO0D,OAAOqB,IAAI,EAAEd,aAAa,CAAC;YAAEe,OAAO;YAAmBlB,QAAQ;QAAO;IAC/E;AACF"}
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { execFile } from "child_process";
|
|
2
|
+
import { writeFile } from "fs/promises";
|
|
3
|
+
import { ghosttyBackend } from "../../terminal/backends/ghostty.js";
|
|
4
|
+
import { TerminalType } from "../../terminal/types.js";
|
|
5
|
+
import { TtyWriter } from "../../terminal/TtyWriter.js";
|
|
6
|
+
import { getProcessCwdsAsync } from "../../utils/process.js";
|
|
7
|
+
vi.mock("child_process", async ()=>{
|
|
8
|
+
const actual = await vi.importActual("child_process");
|
|
9
|
+
return {
|
|
10
|
+
...actual,
|
|
11
|
+
execFile: vi.fn()
|
|
12
|
+
};
|
|
13
|
+
});
|
|
14
|
+
vi.mock("fs/promises", async ()=>{
|
|
15
|
+
const actual = await vi.importActual("fs/promises");
|
|
16
|
+
return {
|
|
17
|
+
...actual,
|
|
18
|
+
writeFile: vi.fn()
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
vi.mock("../../utils/process.js", async ()=>{
|
|
22
|
+
const actual = await vi.importActual("../../utils/process.js");
|
|
23
|
+
return {
|
|
24
|
+
...actual,
|
|
25
|
+
getProcessCwdsAsync: vi.fn()
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
const mockedExecFile = execFile;
|
|
29
|
+
const mockedGetProcessCwds = getProcessCwdsAsync;
|
|
30
|
+
const mockedWriteFile = writeFile;
|
|
31
|
+
const SEP = "\x1f";
|
|
32
|
+
/**
|
|
33
|
+
* Fake `ps`, process cwds and Ghostty AppleScript. `processes` rows are
|
|
34
|
+
* `pid ppid tty comm`; `cwds` maps pid to its working directory. Writing an
|
|
35
|
+
* OSC 2 title to a TTY renames the terminal listed in `ttyOwner`.
|
|
36
|
+
*/ function fakeSystem(opts) {
|
|
37
|
+
const terminals = opts.terminals.map((t)=>({
|
|
38
|
+
...t
|
|
39
|
+
}));
|
|
40
|
+
mockedGetProcessCwds.mockImplementation(async (pids)=>{
|
|
41
|
+
const cwds = new Map();
|
|
42
|
+
for (const pid of pids)if (opts.cwds?.[pid]) cwds.set(pid, opts.cwds[pid]);
|
|
43
|
+
return cwds;
|
|
44
|
+
});
|
|
45
|
+
mockedWriteFile.mockImplementation(async (tty, data)=>{
|
|
46
|
+
// eslint-disable-next-line no-control-regex
|
|
47
|
+
const title = /^\x1b\]2;(.*)\x07$/.exec(data)?.[1];
|
|
48
|
+
const owner = terminals.find((t)=>t.id === opts.ttyOwner?.[tty]);
|
|
49
|
+
if (owner && title !== undefined) owner.name = title;
|
|
50
|
+
});
|
|
51
|
+
mockedExecFile.mockImplementation((...args)=>{
|
|
52
|
+
const [cmd, argv] = args;
|
|
53
|
+
const cb = args[args.length - 1];
|
|
54
|
+
let stdout = "";
|
|
55
|
+
if (cmd === "ps") stdout = opts.processes.join("\n") + "\n";
|
|
56
|
+
else if (cmd === "osascript" && argv[1].includes("repeat with t in terminals")) {
|
|
57
|
+
stdout = terminals.map((t)=>[
|
|
58
|
+
t.id,
|
|
59
|
+
t.name,
|
|
60
|
+
t.dir
|
|
61
|
+
].join(SEP)).join("\n") + "\n";
|
|
62
|
+
} else if (cmd === "osascript") stdout = opts.osascriptResult ?? "ok\n";
|
|
63
|
+
cb(null, {
|
|
64
|
+
stdout
|
|
65
|
+
});
|
|
66
|
+
return {};
|
|
67
|
+
});
|
|
68
|
+
return terminals;
|
|
69
|
+
}
|
|
70
|
+
const GHOSTTY = "500 1 ?? /Applications/Ghostty.app/Contents/MacOS/ghostty";
|
|
71
|
+
describe("Ghostty terminal backend", ()=>{
|
|
72
|
+
beforeEach(()=>{
|
|
73
|
+
vi.clearAllMocks();
|
|
74
|
+
});
|
|
75
|
+
describe("find", ()=>{
|
|
76
|
+
it("ignores TTYs when Ghostty is not running", async ()=>{
|
|
77
|
+
fakeSystem({
|
|
78
|
+
processes: [
|
|
79
|
+
"600 1 ttys001 /usr/bin/login"
|
|
80
|
+
],
|
|
81
|
+
terminals: []
|
|
82
|
+
});
|
|
83
|
+
expect(await ghosttyBackend.find("/dev/ttys001")).toBeNull();
|
|
84
|
+
expect(mockedExecFile).not.toHaveBeenCalledWith("osascript", expect.anything(), expect.anything());
|
|
85
|
+
});
|
|
86
|
+
it("ignores TTYs whose session was not started by Ghostty", async ()=>{
|
|
87
|
+
fakeSystem({
|
|
88
|
+
processes: [
|
|
89
|
+
GHOSTTY,
|
|
90
|
+
"600 500 ttys001 /usr/bin/login",
|
|
91
|
+
"700 1 ttys002 /usr/bin/login"
|
|
92
|
+
],
|
|
93
|
+
terminals: [
|
|
94
|
+
{
|
|
95
|
+
id: "T1",
|
|
96
|
+
name: "zsh",
|
|
97
|
+
dir: "/repo"
|
|
98
|
+
}
|
|
99
|
+
]
|
|
100
|
+
});
|
|
101
|
+
expect(await ghosttyBackend.find("/dev/ttys002")).toBeNull();
|
|
102
|
+
expect(mockedWriteFile).not.toHaveBeenCalled();
|
|
103
|
+
});
|
|
104
|
+
it("matches by working directory when it pairs one terminal with one TTY", async ()=>{
|
|
105
|
+
fakeSystem({
|
|
106
|
+
processes: [
|
|
107
|
+
GHOSTTY,
|
|
108
|
+
"600 500 ttys001 /usr/bin/login",
|
|
109
|
+
"601 600 ttys001 -zsh",
|
|
110
|
+
"602 601 ttys001 claude",
|
|
111
|
+
"700 500 ttys002 /usr/bin/login",
|
|
112
|
+
"701 700 ttys002 -zsh"
|
|
113
|
+
],
|
|
114
|
+
cwds: {
|
|
115
|
+
601: "/repo/a",
|
|
116
|
+
602: "/repo/a",
|
|
117
|
+
701: "/repo/b"
|
|
118
|
+
},
|
|
119
|
+
terminals: [
|
|
120
|
+
{
|
|
121
|
+
id: "T1",
|
|
122
|
+
name: "claude",
|
|
123
|
+
dir: "/repo/a"
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
id: "T2",
|
|
127
|
+
name: "zsh",
|
|
128
|
+
dir: "/repo/b"
|
|
129
|
+
}
|
|
130
|
+
]
|
|
131
|
+
});
|
|
132
|
+
expect(await ghosttyBackend.find("/dev/ttys001")).toEqual({
|
|
133
|
+
type: TerminalType.GHOSTTY,
|
|
134
|
+
identifier: "T1",
|
|
135
|
+
tty: "/dev/ttys001"
|
|
136
|
+
});
|
|
137
|
+
expect(mockedWriteFile).not.toHaveBeenCalled();
|
|
138
|
+
});
|
|
139
|
+
it("uses a title marker when two terminals share the directory, then restores the title", async ()=>{
|
|
140
|
+
const terminals = fakeSystem({
|
|
141
|
+
processes: [
|
|
142
|
+
GHOSTTY,
|
|
143
|
+
"600 500 ttys001 /usr/bin/login",
|
|
144
|
+
"601 600 ttys001 claude",
|
|
145
|
+
"700 500 ttys002 /usr/bin/login",
|
|
146
|
+
"701 700 ttys002 codex"
|
|
147
|
+
],
|
|
148
|
+
cwds: {
|
|
149
|
+
601: "/repo",
|
|
150
|
+
701: "/repo"
|
|
151
|
+
},
|
|
152
|
+
terminals: [
|
|
153
|
+
{
|
|
154
|
+
id: "T1",
|
|
155
|
+
name: "first agent",
|
|
156
|
+
dir: "/repo"
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
id: "T2",
|
|
160
|
+
name: "second agent",
|
|
161
|
+
dir: "/repo"
|
|
162
|
+
}
|
|
163
|
+
],
|
|
164
|
+
ttyOwner: {
|
|
165
|
+
"/dev/ttys001": "T1",
|
|
166
|
+
"/dev/ttys002": "T2"
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
const location = await ghosttyBackend.find("/dev/ttys002");
|
|
170
|
+
expect(location?.identifier).toBe("T2");
|
|
171
|
+
const titles = mockedWriteFile.mock.calls.map(([tty, data])=>[
|
|
172
|
+
tty,
|
|
173
|
+
data
|
|
174
|
+
]);
|
|
175
|
+
expect(titles).toHaveLength(2);
|
|
176
|
+
expect(titles[0][0]).toBe("/dev/ttys002");
|
|
177
|
+
// eslint-disable-next-line no-control-regex
|
|
178
|
+
expect(titles[0][1]).toMatch(/^\x1b\]2;ai-devkit-\d+-\d+\x07$/);
|
|
179
|
+
expect(titles[1]).toEqual([
|
|
180
|
+
"/dev/ttys002",
|
|
181
|
+
"\x1b]2;second agent\x07"
|
|
182
|
+
]);
|
|
183
|
+
expect(terminals.find((t)=>t.id === "T2")?.name).toBe("second agent");
|
|
184
|
+
});
|
|
185
|
+
it("does not trust a unique directory match when another Ghostty TTY has a process there", async ()=>{
|
|
186
|
+
// T1's own working directory is unreported; T2 sits in the agent's directory.
|
|
187
|
+
fakeSystem({
|
|
188
|
+
processes: [
|
|
189
|
+
GHOSTTY,
|
|
190
|
+
"600 500 ttys001 /usr/bin/login",
|
|
191
|
+
"601 600 ttys001 claude",
|
|
192
|
+
"700 500 ttys002 /usr/bin/login",
|
|
193
|
+
"701 700 ttys002 -zsh"
|
|
194
|
+
],
|
|
195
|
+
cwds: {
|
|
196
|
+
601: "/repo",
|
|
197
|
+
701: "/repo"
|
|
198
|
+
},
|
|
199
|
+
terminals: [
|
|
200
|
+
{
|
|
201
|
+
id: "T1",
|
|
202
|
+
name: "claude",
|
|
203
|
+
dir: ""
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
id: "T2",
|
|
207
|
+
name: "zsh",
|
|
208
|
+
dir: "/repo"
|
|
209
|
+
}
|
|
210
|
+
],
|
|
211
|
+
ttyOwner: {
|
|
212
|
+
"/dev/ttys001": "T1",
|
|
213
|
+
"/dev/ttys002": "T2"
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
expect((await ghosttyBackend.find("/dev/ttys001"))?.identifier).toBe("T1");
|
|
217
|
+
expect(mockedWriteFile).toHaveBeenCalled();
|
|
218
|
+
});
|
|
219
|
+
it("returns null when no terminal shows the marker", async ()=>{
|
|
220
|
+
vi.useFakeTimers();
|
|
221
|
+
try {
|
|
222
|
+
fakeSystem({
|
|
223
|
+
processes: [
|
|
224
|
+
GHOSTTY,
|
|
225
|
+
"600 500 ttys001 /usr/bin/login"
|
|
226
|
+
],
|
|
227
|
+
terminals: [
|
|
228
|
+
{
|
|
229
|
+
id: "T1",
|
|
230
|
+
name: "a",
|
|
231
|
+
dir: "/x"
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
id: "T2",
|
|
235
|
+
name: "b",
|
|
236
|
+
dir: "/y"
|
|
237
|
+
}
|
|
238
|
+
]
|
|
239
|
+
});
|
|
240
|
+
const result = ghosttyBackend.find("/dev/ttys001");
|
|
241
|
+
await vi.runAllTimersAsync();
|
|
242
|
+
expect(await result).toBeNull();
|
|
243
|
+
} finally{
|
|
244
|
+
vi.useRealTimers();
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
it("returns null instead of throwing when Ghostty is not scriptable", async ()=>{
|
|
248
|
+
mockedGetProcessCwds.mockResolvedValue(new Map());
|
|
249
|
+
mockedExecFile.mockImplementation((...args)=>{
|
|
250
|
+
const [cmd] = args;
|
|
251
|
+
const cb = args[args.length - 1];
|
|
252
|
+
if (cmd === "osascript") cb(new Error("Not authorized to send Apple events"));
|
|
253
|
+
else cb(null, {
|
|
254
|
+
stdout: `${GHOSTTY}\n600 500 ttys001 /usr/bin/login\n`
|
|
255
|
+
});
|
|
256
|
+
return {};
|
|
257
|
+
});
|
|
258
|
+
expect(await ghosttyBackend.find("/dev/ttys001")).toBeNull();
|
|
259
|
+
});
|
|
260
|
+
});
|
|
261
|
+
describe("focus, send and sendKey", ()=>{
|
|
262
|
+
const location = {
|
|
263
|
+
type: TerminalType.GHOSTTY,
|
|
264
|
+
identifier: "T1",
|
|
265
|
+
tty: "/dev/ttys001"
|
|
266
|
+
};
|
|
267
|
+
const scripts = ()=>mockedExecFile.mock.calls.filter(([cmd])=>cmd === "osascript").map(([, argv])=>argv[1]);
|
|
268
|
+
it("focuses the terminal by id and activates Ghostty", async ()=>{
|
|
269
|
+
fakeSystem({
|
|
270
|
+
processes: [],
|
|
271
|
+
terminals: []
|
|
272
|
+
});
|
|
273
|
+
expect(await ghosttyBackend.focus(location)).toBe(true);
|
|
274
|
+
expect(scripts()[0]).toContain('every terminal whose id is "T1"');
|
|
275
|
+
expect(scripts()[0]).toContain("focus t");
|
|
276
|
+
expect(scripts()[0]).toContain("activate");
|
|
277
|
+
});
|
|
278
|
+
it("reports a failed focus when the terminal is gone", async ()=>{
|
|
279
|
+
fakeSystem({
|
|
280
|
+
processes: [],
|
|
281
|
+
terminals: [],
|
|
282
|
+
osascriptResult: "not_found\n"
|
|
283
|
+
});
|
|
284
|
+
expect(await ghosttyBackend.focus(location)).toBe(false);
|
|
285
|
+
});
|
|
286
|
+
it("pastes the message with input text, then types a carriage return", async ()=>{
|
|
287
|
+
fakeSystem({
|
|
288
|
+
processes: [],
|
|
289
|
+
terminals: []
|
|
290
|
+
});
|
|
291
|
+
await TtyWriter.send(location, 'say "hi"\nnext');
|
|
292
|
+
expect(scripts()).toHaveLength(2);
|
|
293
|
+
expect(scripts()[0]).toContain('input text "say \\"hi\\"\\nnext" to t');
|
|
294
|
+
expect(scripts()[1]).toContain('perform action "text:\\\\x0d" on t');
|
|
295
|
+
});
|
|
296
|
+
it("throws when the terminal is gone", async ()=>{
|
|
297
|
+
fakeSystem({
|
|
298
|
+
processes: [],
|
|
299
|
+
terminals: [],
|
|
300
|
+
osascriptResult: "not_found\n"
|
|
301
|
+
});
|
|
302
|
+
await expect(TtyWriter.send(location, "hello")).rejects.toThrow("Ghostty terminal not found for TTY /dev/ttys001");
|
|
303
|
+
});
|
|
304
|
+
// Ghostty parses `text:` with Zig escapes; AppleScript doubles each backslash.
|
|
305
|
+
it.each([
|
|
306
|
+
[
|
|
307
|
+
"2",
|
|
308
|
+
"text:2"
|
|
309
|
+
],
|
|
310
|
+
[
|
|
311
|
+
"\x1b",
|
|
312
|
+
"text:\\\\x1b"
|
|
313
|
+
],
|
|
314
|
+
[
|
|
315
|
+
"\r",
|
|
316
|
+
"text:\\\\x0d"
|
|
317
|
+
],
|
|
318
|
+
[
|
|
319
|
+
"\\",
|
|
320
|
+
"text:\\\\\\\\"
|
|
321
|
+
]
|
|
322
|
+
])("types key %j with the Ghostty action %s", async (key, action)=>{
|
|
323
|
+
fakeSystem({
|
|
324
|
+
processes: [],
|
|
325
|
+
terminals: []
|
|
326
|
+
});
|
|
327
|
+
await TtyWriter.sendKey(location, key);
|
|
328
|
+
expect(scripts()[0]).toContain(`perform action "${action}" on t`);
|
|
329
|
+
expect(scripts()[0]).not.toContain("send key");
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
//# sourceMappingURL=GhosttyBackend.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/__tests__/terminal/GhosttyBackend.test.ts"],"sourcesContent":["import { execFile } from \"child_process\";\nimport { writeFile } from \"fs/promises\";\nimport type { Mock } from \"vitest\";\n\nimport { ghosttyBackend } from \"../../terminal/backends/ghostty.js\";\nimport { TerminalType, type TerminalLocation } from \"../../terminal/types.js\";\nimport { TtyWriter } from \"../../terminal/TtyWriter.js\";\nimport { getProcessCwdsAsync } from \"../../utils/process.js\";\n\nvi.mock(\"child_process\", async () => {\n const actual = await vi.importActual<typeof import(\"child_process\")>(\"child_process\");\n return { ...actual, execFile: vi.fn() };\n});\n\nvi.mock(\"fs/promises\", async () => {\n const actual = await vi.importActual<typeof import(\"fs/promises\")>(\"fs/promises\");\n return { ...actual, writeFile: vi.fn() };\n});\n\nvi.mock(\"../../utils/process.js\", async () => {\n const actual =\n await vi.importActual<typeof import(\"../../utils/process.js\")>(\"../../utils/process.js\");\n return { ...actual, getProcessCwdsAsync: vi.fn() };\n});\n\nconst mockedExecFile = execFile as unknown as Mock;\nconst mockedGetProcessCwds = getProcessCwdsAsync as unknown as Mock;\nconst mockedWriteFile = writeFile as unknown as Mock;\nconst SEP = \"\\x1f\";\n\ninterface FakeTerminal {\n id: string;\n name: string;\n dir: string;\n}\n\n/**\n * Fake `ps`, process cwds and Ghostty AppleScript. `processes` rows are\n * `pid ppid tty comm`; `cwds` maps pid to its working directory. Writing an\n * OSC 2 title to a TTY renames the terminal listed in `ttyOwner`.\n */\nfunction fakeSystem(opts: {\n processes: string[];\n cwds?: Record<number, string>;\n terminals: FakeTerminal[];\n ttyOwner?: Record<string, string>;\n osascriptResult?: string;\n}) {\n const terminals = opts.terminals.map((t) => ({ ...t }));\n mockedGetProcessCwds.mockImplementation(async (pids: number[]) => {\n const cwds = new Map<number, string>();\n for (const pid of pids) if (opts.cwds?.[pid]) cwds.set(pid, opts.cwds[pid]);\n return cwds;\n });\n mockedWriteFile.mockImplementation(async (tty: string, data: string) => {\n // eslint-disable-next-line no-control-regex\n const title = /^\\x1b\\]2;(.*)\\x07$/.exec(data)?.[1];\n const owner = terminals.find((t) => t.id === opts.ttyOwner?.[tty]);\n if (owner && title !== undefined) owner.name = title;\n });\n mockedExecFile.mockImplementation((...args: unknown[]) => {\n const [cmd, argv] = args as [string, string[]];\n const cb = args[args.length - 1] as (err: Error | null, r?: { stdout: string }) => void;\n let stdout = \"\";\n if (cmd === \"ps\") stdout = opts.processes.join(\"\\n\") + \"\\n\";\n else if (cmd === \"osascript\" && argv[1].includes(\"repeat with t in terminals\")) {\n stdout = terminals.map((t) => [t.id, t.name, t.dir].join(SEP)).join(\"\\n\") + \"\\n\";\n } else if (cmd === \"osascript\") stdout = opts.osascriptResult ?? \"ok\\n\";\n cb(null, { stdout });\n return {};\n });\n return terminals;\n}\n\nconst GHOSTTY = \"500 1 ?? /Applications/Ghostty.app/Contents/MacOS/ghostty\";\n\ndescribe(\"Ghostty terminal backend\", () => {\n beforeEach(() => {\n vi.clearAllMocks();\n });\n\n describe(\"find\", () => {\n it(\"ignores TTYs when Ghostty is not running\", async () => {\n fakeSystem({ processes: [\"600 1 ttys001 /usr/bin/login\"], terminals: [] });\n\n expect(await ghosttyBackend.find(\"/dev/ttys001\")).toBeNull();\n expect(mockedExecFile).not.toHaveBeenCalledWith(\n \"osascript\",\n expect.anything(),\n expect.anything(),\n );\n });\n\n it(\"ignores TTYs whose session was not started by Ghostty\", async () => {\n fakeSystem({\n processes: [GHOSTTY, \"600 500 ttys001 /usr/bin/login\", \"700 1 ttys002 /usr/bin/login\"],\n terminals: [{ id: \"T1\", name: \"zsh\", dir: \"/repo\" }],\n });\n\n expect(await ghosttyBackend.find(\"/dev/ttys002\")).toBeNull();\n expect(mockedWriteFile).not.toHaveBeenCalled();\n });\n\n it(\"matches by working directory when it pairs one terminal with one TTY\", async () => {\n fakeSystem({\n processes: [\n GHOSTTY,\n \"600 500 ttys001 /usr/bin/login\",\n \"601 600 ttys001 -zsh\",\n \"602 601 ttys001 claude\",\n \"700 500 ttys002 /usr/bin/login\",\n \"701 700 ttys002 -zsh\",\n ],\n cwds: { 601: \"/repo/a\", 602: \"/repo/a\", 701: \"/repo/b\" },\n terminals: [\n { id: \"T1\", name: \"claude\", dir: \"/repo/a\" },\n { id: \"T2\", name: \"zsh\", dir: \"/repo/b\" },\n ],\n });\n\n expect(await ghosttyBackend.find(\"/dev/ttys001\")).toEqual({\n type: TerminalType.GHOSTTY,\n identifier: \"T1\",\n tty: \"/dev/ttys001\",\n });\n expect(mockedWriteFile).not.toHaveBeenCalled();\n });\n\n it(\"uses a title marker when two terminals share the directory, then restores the title\", async () => {\n const terminals = fakeSystem({\n processes: [\n GHOSTTY,\n \"600 500 ttys001 /usr/bin/login\",\n \"601 600 ttys001 claude\",\n \"700 500 ttys002 /usr/bin/login\",\n \"701 700 ttys002 codex\",\n ],\n cwds: { 601: \"/repo\", 701: \"/repo\" },\n terminals: [\n { id: \"T1\", name: \"first agent\", dir: \"/repo\" },\n { id: \"T2\", name: \"second agent\", dir: \"/repo\" },\n ],\n ttyOwner: { \"/dev/ttys001\": \"T1\", \"/dev/ttys002\": \"T2\" },\n });\n\n const location = await ghosttyBackend.find(\"/dev/ttys002\");\n\n expect(location?.identifier).toBe(\"T2\");\n const titles = mockedWriteFile.mock.calls.map(([tty, data]) => [tty, data]);\n expect(titles).toHaveLength(2);\n expect(titles[0][0]).toBe(\"/dev/ttys002\");\n // eslint-disable-next-line no-control-regex\n expect(titles[0][1]).toMatch(/^\\x1b\\]2;ai-devkit-\\d+-\\d+\\x07$/);\n expect(titles[1]).toEqual([\"/dev/ttys002\", \"\\x1b]2;second agent\\x07\"]);\n expect(terminals.find((t) => t.id === \"T2\")?.name).toBe(\"second agent\");\n });\n\n it(\"does not trust a unique directory match when another Ghostty TTY has a process there\", async () => {\n // T1's own working directory is unreported; T2 sits in the agent's directory.\n fakeSystem({\n processes: [\n GHOSTTY,\n \"600 500 ttys001 /usr/bin/login\",\n \"601 600 ttys001 claude\",\n \"700 500 ttys002 /usr/bin/login\",\n \"701 700 ttys002 -zsh\",\n ],\n cwds: { 601: \"/repo\", 701: \"/repo\" },\n terminals: [\n { id: \"T1\", name: \"claude\", dir: \"\" },\n { id: \"T2\", name: \"zsh\", dir: \"/repo\" },\n ],\n ttyOwner: { \"/dev/ttys001\": \"T1\", \"/dev/ttys002\": \"T2\" },\n });\n\n expect((await ghosttyBackend.find(\"/dev/ttys001\"))?.identifier).toBe(\"T1\");\n expect(mockedWriteFile).toHaveBeenCalled();\n });\n\n it(\"returns null when no terminal shows the marker\", async () => {\n vi.useFakeTimers();\n try {\n fakeSystem({\n processes: [GHOSTTY, \"600 500 ttys001 /usr/bin/login\"],\n terminals: [\n { id: \"T1\", name: \"a\", dir: \"/x\" },\n { id: \"T2\", name: \"b\", dir: \"/y\" },\n ],\n });\n\n const result = ghosttyBackend.find(\"/dev/ttys001\");\n await vi.runAllTimersAsync();\n\n expect(await result).toBeNull();\n } finally {\n vi.useRealTimers();\n }\n });\n\n it(\"returns null instead of throwing when Ghostty is not scriptable\", async () => {\n mockedGetProcessCwds.mockResolvedValue(new Map());\n mockedExecFile.mockImplementation((...args: unknown[]) => {\n const [cmd] = args as [string];\n const cb = args[args.length - 1] as (err: Error | null, r?: { stdout: string }) => void;\n if (cmd === \"osascript\") cb(new Error(\"Not authorized to send Apple events\"));\n else cb(null, { stdout: `${GHOSTTY}\\n600 500 ttys001 /usr/bin/login\\n` });\n return {};\n });\n\n expect(await ghosttyBackend.find(\"/dev/ttys001\")).toBeNull();\n });\n });\n\n describe(\"focus, send and sendKey\", () => {\n const location: TerminalLocation = {\n type: TerminalType.GHOSTTY,\n identifier: \"T1\",\n tty: \"/dev/ttys001\",\n };\n const scripts = () =>\n mockedExecFile.mock.calls.filter(([cmd]) => cmd === \"osascript\").map(([, argv]) => argv[1]);\n\n it(\"focuses the terminal by id and activates Ghostty\", async () => {\n fakeSystem({ processes: [], terminals: [] });\n\n expect(await ghosttyBackend.focus(location)).toBe(true);\n expect(scripts()[0]).toContain('every terminal whose id is \"T1\"');\n expect(scripts()[0]).toContain(\"focus t\");\n expect(scripts()[0]).toContain(\"activate\");\n });\n\n it(\"reports a failed focus when the terminal is gone\", async () => {\n fakeSystem({ processes: [], terminals: [], osascriptResult: \"not_found\\n\" });\n\n expect(await ghosttyBackend.focus(location)).toBe(false);\n });\n\n it(\"pastes the message with input text, then types a carriage return\", async () => {\n fakeSystem({ processes: [], terminals: [] });\n\n await TtyWriter.send(location, 'say \"hi\"\\nnext');\n\n expect(scripts()).toHaveLength(2);\n expect(scripts()[0]).toContain('input text \"say \\\\\"hi\\\\\"\\\\nnext\" to t');\n expect(scripts()[1]).toContain('perform action \"text:\\\\\\\\x0d\" on t');\n });\n\n it(\"throws when the terminal is gone\", async () => {\n fakeSystem({ processes: [], terminals: [], osascriptResult: \"not_found\\n\" });\n\n await expect(TtyWriter.send(location, \"hello\")).rejects.toThrow(\n \"Ghostty terminal not found for TTY /dev/ttys001\",\n );\n });\n\n // Ghostty parses `text:` with Zig escapes; AppleScript doubles each backslash.\n it.each([\n [\"2\", \"text:2\"],\n [\"\\x1b\", \"text:\\\\\\\\x1b\"],\n [\"\\r\", \"text:\\\\\\\\x0d\"],\n [\"\\\\\", \"text:\\\\\\\\\\\\\\\\\"],\n ])(\"types key %j with the Ghostty action %s\", async (key, action) => {\n fakeSystem({ processes: [], terminals: [] });\n\n await TtyWriter.sendKey(location, key);\n\n expect(scripts()[0]).toContain(`perform action \"${action}\" on t`);\n expect(scripts()[0]).not.toContain(\"send key\");\n });\n });\n});\n"],"names":["execFile","writeFile","ghosttyBackend","TerminalType","TtyWriter","getProcessCwdsAsync","vi","mock","actual","importActual","fn","mockedExecFile","mockedGetProcessCwds","mockedWriteFile","SEP","fakeSystem","opts","terminals","map","t","mockImplementation","pids","cwds","Map","pid","set","tty","data","title","exec","owner","find","id","ttyOwner","undefined","name","args","cmd","argv","cb","length","stdout","processes","join","includes","dir","osascriptResult","GHOSTTY","describe","beforeEach","clearAllMocks","it","expect","toBeNull","not","toHaveBeenCalledWith","anything","toHaveBeenCalled","toEqual","type","identifier","location","toBe","titles","calls","toHaveLength","toMatch","useFakeTimers","result","runAllTimersAsync","useRealTimers","mockResolvedValue","Error","scripts","filter","focus","toContain","send","rejects","toThrow","each","key","action","sendKey"],"mappings":"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"}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Each adapter is responsible for detecting agents of a specific type (e.g., claude).
|
|
6
6
|
*/
|
|
7
7
|
/** Every built-in harness type, in the order the CLI lists them. */
|
|
8
|
-
export declare const AGENT_TYPES: readonly ["claude", "codex", "gemini_cli", "grok_cli", "kiro", "antigravity_cli", "opencode", "copilot", "pi"];
|
|
8
|
+
export declare const AGENT_TYPES: readonly ["claude", "codex", "gemini_cli", "grok_cli", "kiro", "antigravity_cli", "opencode", "copilot", "pi", "devin"];
|
|
9
9
|
/**
|
|
10
10
|
* Type of AI agent
|
|
11
11
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AgentAdapter.d.ts","sourceRoot":"","sources":["../../src/adapters/AgentAdapter.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,oEAAoE;AACpE,eAAO,MAAM,WAAW,
|
|
1
|
+
{"version":3,"file":"AgentAdapter.d.ts","sourceRoot":"","sources":["../../src/adapters/AgentAdapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/adapters/AgentAdapter.ts"],"sourcesContent":["/**\n * Agent Adapter Interface\n *\n * Defines the contract for detecting and managing different types of AI agents.\n * Each adapter is responsible for detecting agents of a specific type (e.g., claude).\n */\n\n/** Every built-in harness type, in the order the CLI lists them. */\nexport const AGENT_TYPES = [\n \"claude\",\n \"codex\",\n \"gemini_cli\",\n \"grok_cli\",\n \"kiro\",\n \"antigravity_cli\",\n \"opencode\",\n \"copilot\",\n \"pi\",\n] as const;\n\n/**\n * Type of AI agent\n */\nexport type AgentType = (typeof AGENT_TYPES)[number] | \"other\";\n\n/**\n * Current status of an agent\n */\nexport enum AgentStatus {\n RUNNING = \"running\",\n WAITING = \"waiting\",\n IDLE = \"idle\",\n UNKNOWN = \"unknown\",\n}\n\n/**\n * Information about a detected agent\n */\nexport interface AgentInfo {\n /** Project-based name (e.g., \"ai-devkit\" or \"ai-devkit (merry)\") */\n name: string;\n\n /** Type of agent */\n type: AgentType;\n\n /** Current status */\n status: AgentStatus;\n\n /** Last user prompt from history */\n summary: string;\n\n /** Process ID */\n pid: number;\n\n /** Working directory/project path */\n projectPath: string;\n\n /** Session UUID */\n sessionId: string;\n\n /** Timestamp of last activity */\n lastActive: Date;\n\n /** Whether the live process is pinned in the agent console */\n pinned?: boolean;\n\n /** Path to the session JSONL file on disk */\n sessionFilePath?: string;\n}\n\n/**\n * Information about a running process\n */\nexport interface ProcessInfo {\n /** Process ID */\n pid: number;\n\n /** Parent process ID, populated by process discovery when available */\n ppid?: number;\n\n /** Process command */\n command: string;\n\n /** Working directory */\n cwd: string;\n\n /** Terminal TTY (e.g., \"ttys030\") */\n tty: string;\n\n /** Process start time, populated by process enrichment */\n startTime?: Date;\n}\n\n/**\n * A single message in a conversation\n */\nexport interface ConversationMessage {\n role: \"user\" | \"assistant\" | \"system\";\n content: string;\n timestamp?: string;\n}\n\n/**\n * Options for {@link AgentAdapter.getConversation}.\n */\nexport interface ConversationOptions {\n /** Include tool call/result details */\n verbose?: boolean;\n /**\n * Return only the last `tail` messages (after verbose filtering). When\n * omitted or not a positive integer, the full conversation is returned.\n *\n * JSONL-backed adapters read backward from the end of the file and, on\n * later calls, parse only appended bytes, so cost is proportional to the\n * tail rather than the transcript size. Adapters backed by a single JSON\n * document (Gemini) or a database (OpenCode) still read the whole source\n * and slice; callers should keep an mtime cache for those.\n */\n tail?: number;\n}\n\n/**\n * A historical session discovered on disk (running or not).\n *\n * Used by `listSessions` to surface enough context for a user to identify\n * a session and resume it via the originating tool's resume command.\n */\nexport interface SessionSummary {\n /** Tool that produced this session */\n type: AgentType;\n\n /**\n * ID accepted by the tool's resume command. Adapters MUST pass this\n * through verbatim — no normalization, no encoding/decoding — so it\n * round-trips into `claude --resume <id>` (and equivalents).\n */\n sessionId: string;\n\n /** Working directory the session was started in (best-known value) */\n cwd: string;\n\n /**\n * Trimmed first user message; empty string if none. Adapters apply\n * the same noise-filter their existing parsers use (skip tool_result\n * blocks, request-interruption notices, system-injected skill\n * content). The CLI table renderer substitutes a placeholder for\n * empty values; JSON output keeps the empty string raw.\n */\n firstUserMessage: string;\n\n /** Last activity timestamp (from session content; falls back to file mtime) */\n lastActive: Date;\n\n /** Session start time (from session content; falls back to file birthtime/mtime) */\n startedAt: Date;\n\n /** Absolute path to the session file on disk (debug/diagnostics) */\n sessionFilePath: string;\n}\n\n/**\n * Filters passed by the CLI to {@link AgentAdapter.listSessions}.\n *\n * The CLI is the source of truth for filter defaults and semantics\n * (e.g. cwd defaults to process.cwd(); --all clears it). Adapters apply\n * the values they receive — they don't invent defaults.\n */\nexport interface ListSessionsOptions {\n /**\n * Filter to sessions whose recorded cwd matches this path using strict\n * equality (no prefix/ancestor matching in v1). Undefined = no cwd\n * filter.\n */\n cwd?: string;\n\n /**\n * Filter to a single tool. Enforced by `AgentManager.listSessions`,\n * which skips adapters whose `type` doesn't match. Adapters MAY\n * ignore this field — by the time their `listSessions` runs, the\n * type filter is already satisfied. Undefined = include every\n * registered adapter.\n */\n type?: AgentType;\n}\n\nexport interface AgentDetectionContext {\n /** One enriched process snapshot shared across this manager refresh. */\n readonly processes: readonly ProcessInfo[];\n}\n\n/**\n * Agent Adapter Interface\n *\n * Implementations must provide detection logic for a specific agent type.\n */\nexport interface AgentAdapter {\n /** Type of agent this adapter handles */\n readonly type: AgentType;\n\n /** Executable basenames required for shared process discovery. */\n readonly processNames?: readonly string[];\n\n /**\n * Detect running agents of this type\n * @returns List of detected agents\n */\n detectAgents(context?: AgentDetectionContext): Promise<AgentInfo[]>;\n\n /**\n * Check if this adapter can handle the given process\n * @param processInfo Process information\n * @returns True if this adapter can handle the process\n */\n canHandle(processInfo: ProcessInfo): boolean;\n\n /**\n * Read the conversation from a session file\n * @param sessionFilePath Path to the session JSONL file\n * @param options.verbose Include tool call/result details\n * @param options.tail Return only the last N messages (see {@link ConversationOptions.tail})\n * @returns Array of conversation messages\n */\n getConversation(sessionFilePath: string, options?: ConversationOptions): ConversationMessage[];\n\n /**\n * Enumerate historical sessions for this tool from disk.\n *\n * Applies `opts.cwd` as a strict-equality filter when set. Returns\n * {@link SessionSummary} entries unsorted; sorting and global filters\n * are handled by `AgentManager` and the CLI.\n *\n * @param opts Filter options computed by the CLI\n * @returns Array of sessions discovered on disk\n */\n listSessions(opts?: ListSessionsOptions): Promise<SessionSummary[]>;\n\n /**\n * Resolve exact historical session matches without enumerating every summary.\n *\n * Built-in adapters implement this using their harness-native storage shape.\n * The method remains optional so external adapters can fall back to\n * {@link listSessions} until they adopt direct lookup.\n */\n findSessionsById?(sessionId: string): Promise<SessionSummary[]>;\n}\n"],"names":["AGENT_TYPES","AgentStatus"],"mappings":"AAAA;;;;;CAKC,GAED,kEAAkE,GAClE,OAAO,MAAMA,cAAc;IACzB;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAAU;AAOX;;CAEC,GACD,OAAO,IAAA,AAAKC,qCAAAA;;;;;WAAAA;MAKX"}
|
|
1
|
+
{"version":3,"sources":["../../src/adapters/AgentAdapter.ts"],"sourcesContent":["/**\n * Agent Adapter Interface\n *\n * Defines the contract for detecting and managing different types of AI agents.\n * Each adapter is responsible for detecting agents of a specific type (e.g., claude).\n */\n\n/** Every built-in harness type, in the order the CLI lists them. */\nexport const AGENT_TYPES = [\n \"claude\",\n \"codex\",\n \"gemini_cli\",\n \"grok_cli\",\n \"kiro\",\n \"antigravity_cli\",\n \"opencode\",\n \"copilot\",\n \"pi\",\n \"devin\",\n] as const;\n\n/**\n * Type of AI agent\n */\nexport type AgentType = (typeof AGENT_TYPES)[number] | \"other\";\n\n/**\n * Current status of an agent\n */\nexport enum AgentStatus {\n RUNNING = \"running\",\n WAITING = \"waiting\",\n IDLE = \"idle\",\n UNKNOWN = \"unknown\",\n}\n\n/**\n * Information about a detected agent\n */\nexport interface AgentInfo {\n /** Project-based name (e.g., \"ai-devkit\" or \"ai-devkit (merry)\") */\n name: string;\n\n /** Type of agent */\n type: AgentType;\n\n /** Current status */\n status: AgentStatus;\n\n /** Last user prompt from history */\n summary: string;\n\n /** Process ID */\n pid: number;\n\n /** Working directory/project path */\n projectPath: string;\n\n /** Session UUID */\n sessionId: string;\n\n /** Timestamp of last activity */\n lastActive: Date;\n\n /** Whether the live process is pinned in the agent console */\n pinned?: boolean;\n\n /** Path to the session JSONL file on disk */\n sessionFilePath?: string;\n}\n\n/**\n * Information about a running process\n */\nexport interface ProcessInfo {\n /** Process ID */\n pid: number;\n\n /** Parent process ID, populated by process discovery when available */\n ppid?: number;\n\n /** Process command */\n command: string;\n\n /** Working directory */\n cwd: string;\n\n /** Terminal TTY (e.g., \"ttys030\") */\n tty: string;\n\n /** Process start time, populated by process enrichment */\n startTime?: Date;\n}\n\n/**\n * A single message in a conversation\n */\nexport interface ConversationMessage {\n role: \"user\" | \"assistant\" | \"system\";\n content: string;\n timestamp?: string;\n}\n\n/**\n * Options for {@link AgentAdapter.getConversation}.\n */\nexport interface ConversationOptions {\n /** Include tool call/result details */\n verbose?: boolean;\n /**\n * Return only the last `tail` messages (after verbose filtering). When\n * omitted or not a positive integer, the full conversation is returned.\n *\n * JSONL-backed adapters read backward from the end of the file and, on\n * later calls, parse only appended bytes, so cost is proportional to the\n * tail rather than the transcript size. Adapters backed by a single JSON\n * document (Gemini) or a database (OpenCode) still read the whole source\n * and slice; callers should keep an mtime cache for those.\n */\n tail?: number;\n}\n\n/**\n * A historical session discovered on disk (running or not).\n *\n * Used by `listSessions` to surface enough context for a user to identify\n * a session and resume it via the originating tool's resume command.\n */\nexport interface SessionSummary {\n /** Tool that produced this session */\n type: AgentType;\n\n /**\n * ID accepted by the tool's resume command. Adapters MUST pass this\n * through verbatim — no normalization, no encoding/decoding — so it\n * round-trips into `claude --resume <id>` (and equivalents).\n */\n sessionId: string;\n\n /** Working directory the session was started in (best-known value) */\n cwd: string;\n\n /**\n * Trimmed first user message; empty string if none. Adapters apply\n * the same noise-filter their existing parsers use (skip tool_result\n * blocks, request-interruption notices, system-injected skill\n * content). The CLI table renderer substitutes a placeholder for\n * empty values; JSON output keeps the empty string raw.\n */\n firstUserMessage: string;\n\n /** Last activity timestamp (from session content; falls back to file mtime) */\n lastActive: Date;\n\n /** Session start time (from session content; falls back to file birthtime/mtime) */\n startedAt: Date;\n\n /** Absolute path to the session file on disk (debug/diagnostics) */\n sessionFilePath: string;\n}\n\n/**\n * Filters passed by the CLI to {@link AgentAdapter.listSessions}.\n *\n * The CLI is the source of truth for filter defaults and semantics\n * (e.g. cwd defaults to process.cwd(); --all clears it). Adapters apply\n * the values they receive — they don't invent defaults.\n */\nexport interface ListSessionsOptions {\n /**\n * Filter to sessions whose recorded cwd matches this path using strict\n * equality (no prefix/ancestor matching in v1). Undefined = no cwd\n * filter.\n */\n cwd?: string;\n\n /**\n * Filter to a single tool. Enforced by `AgentManager.listSessions`,\n * which skips adapters whose `type` doesn't match. Adapters MAY\n * ignore this field — by the time their `listSessions` runs, the\n * type filter is already satisfied. Undefined = include every\n * registered adapter.\n */\n type?: AgentType;\n}\n\nexport interface AgentDetectionContext {\n /** One enriched process snapshot shared across this manager refresh. */\n readonly processes: readonly ProcessInfo[];\n}\n\n/**\n * Agent Adapter Interface\n *\n * Implementations must provide detection logic for a specific agent type.\n */\nexport interface AgentAdapter {\n /** Type of agent this adapter handles */\n readonly type: AgentType;\n\n /** Executable basenames required for shared process discovery. */\n readonly processNames?: readonly string[];\n\n /**\n * Detect running agents of this type\n * @returns List of detected agents\n */\n detectAgents(context?: AgentDetectionContext): Promise<AgentInfo[]>;\n\n /**\n * Check if this adapter can handle the given process\n * @param processInfo Process information\n * @returns True if this adapter can handle the process\n */\n canHandle(processInfo: ProcessInfo): boolean;\n\n /**\n * Read the conversation from a session file\n * @param sessionFilePath Path to the session JSONL file\n * @param options.verbose Include tool call/result details\n * @param options.tail Return only the last N messages (see {@link ConversationOptions.tail})\n * @returns Array of conversation messages\n */\n getConversation(sessionFilePath: string, options?: ConversationOptions): ConversationMessage[];\n\n /**\n * Enumerate historical sessions for this tool from disk.\n *\n * Applies `opts.cwd` as a strict-equality filter when set. Returns\n * {@link SessionSummary} entries unsorted; sorting and global filters\n * are handled by `AgentManager` and the CLI.\n *\n * @param opts Filter options computed by the CLI\n * @returns Array of sessions discovered on disk\n */\n listSessions(opts?: ListSessionsOptions): Promise<SessionSummary[]>;\n\n /**\n * Resolve exact historical session matches without enumerating every summary.\n *\n * Built-in adapters implement this using their harness-native storage shape.\n * The method remains optional so external adapters can fall back to\n * {@link listSessions} until they adopt direct lookup.\n */\n findSessionsById?(sessionId: string): Promise<SessionSummary[]>;\n}\n"],"names":["AGENT_TYPES","AgentStatus"],"mappings":"AAAA;;;;;CAKC,GAED,kEAAkE,GAClE,OAAO,MAAMA,cAAc;IACzB;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,CAAU;AAOX;;CAEC,GACD,OAAO,IAAA,AAAKC,qCAAAA;;;;;WAAAA;MAKX"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Devin Adapter
|
|
3
|
+
*
|
|
4
|
+
* Detects running Devin CLI agents by:
|
|
5
|
+
* 1. Filtering `devin` processes from a shared process snapshot, rejecting
|
|
6
|
+
* utility subcommands (list, doctor, ssh, ...) and `devin acp` children
|
|
7
|
+
* spawned by a Devin TUI
|
|
8
|
+
* 2. Matching processes to sessions via `session_locks/<slug>.lock` files,
|
|
9
|
+
* which contain the PID of the session's `devin acp` backend (lock-first;
|
|
10
|
+
* a TUI is matched through its backend child's ppid)
|
|
11
|
+
* 3. Falling back to `sessions.working_directory` matching in
|
|
12
|
+
* ~/.local/share/devin/cli/sessions.db (WAL SQLite, opened read-only)
|
|
13
|
+
*
|
|
14
|
+
* sessionFilePath encodes "<dbPath>::<sessionSlug>" so getConversation() can
|
|
15
|
+
* address a session row without extending the AgentAdapter interface.
|
|
16
|
+
*/
|
|
17
|
+
import type { AgentAdapter, AgentInfo, ProcessInfo, ConversationMessage, ConversationOptions, SessionSummary, ListSessionsOptions, AgentDetectionContext } from "../../adapters/AgentAdapter.js";
|
|
18
|
+
export declare class DevinAdapter implements AgentAdapter {
|
|
19
|
+
readonly type: "devin";
|
|
20
|
+
readonly processNames: readonly ["devin"];
|
|
21
|
+
private readonly parser;
|
|
22
|
+
private readonly locator;
|
|
23
|
+
private readonly mapper;
|
|
24
|
+
private readonly cleanup;
|
|
25
|
+
constructor(dbPath?: string, locksDir?: string);
|
|
26
|
+
close(): void;
|
|
27
|
+
canHandle(processInfo: ProcessInfo): boolean;
|
|
28
|
+
detectAgents(context?: AgentDetectionContext): Promise<AgentInfo[]>;
|
|
29
|
+
getConversation(sessionFilePath: string, options?: ConversationOptions): ConversationMessage[];
|
|
30
|
+
listSessions(opts?: ListSessionsOptions): Promise<SessionSummary[]>;
|
|
31
|
+
findSessionsById(sessionId: string): Promise<SessionSummary[]>;
|
|
32
|
+
/**
|
|
33
|
+
* A `devin acp` process spawned by another devin process is that session's
|
|
34
|
+
* backend, not a separate agent. Standalone `acp` processes (external ACP
|
|
35
|
+
* clients) keep their own agent entry.
|
|
36
|
+
*/
|
|
37
|
+
private isAcpBackendChild;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=DevinAdapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DevinAdapter.d.ts","sourceRoot":"","sources":["../../../src/harnesses/devin/DevinAdapter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,KAAK,EACV,YAAY,EACZ,SAAS,EACT,WAAW,EACX,mBAAmB,EACnB,mBAAmB,EACnB,cAAc,EACd,mBAAmB,EACnB,qBAAqB,EACtB,MAAM,gCAAgC,CAAC;AAyFxC,qBAAa,YAAa,YAAW,YAAY;IAC/C,QAAQ,CAAC,IAAI,EAAG,OAAO,CAAU;IACjC,QAAQ,CAAC,YAAY,qBAAsB;IAE3C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAqB;IAC5C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAsB;IAC9C,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmB;IAC1C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4B;gBAExC,MAAM,CAAC,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM;IAU9C,KAAK,IAAI,IAAI;IAOb,SAAS,CAAC,WAAW,EAAE,WAAW,GAAG,OAAO;IAMtC,YAAY,CAAC,OAAO,CAAC,EAAE,qBAAqB,GAAG,OAAO,CAAC,SAAS,EAAE,CAAC;IA6CzE,eAAe,CAAC,eAAe,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,mBAAmB,GAAG,mBAAmB,EAAE;IAUxF,YAAY,CAAC,IAAI,CAAC,EAAE,mBAAmB,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAInE,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAIpE;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;CAQ1B"}
|