Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
186 changes: 186 additions & 0 deletions UPSTREAM_DEPENDENCIES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
# HAPI 上游依赖分析与合并指南

## 项目概述

**HAPI** 是一个支持多AI代理(Claude、Codex、Gemini、OpenCode)的会话管理和代码执行平台。

- **Fork源**: https://github.com/tiann/hapi.git (upstream)
- **当前仓库**: https://github.com/LosEcher/hapi.git (origin)

## 核心依赖关系分析

### 1. AI Agent 支持

项目对Claude和Codex有**中等强度依赖**:

#### Claude 依赖点
| 文件路径 | 依赖内容 | 依赖强度 |
|---------|---------|---------|
| `cli/src/modules/common/slashCommands.ts:28-34` | Built-in slash commands定义 | 中等 |
| `cli/src/modules/common/slashCommands.ts:86-91` | `CLAUDE_CONFIG_DIR`环境变量 | 弱 |
| `cli/src/modules/common/slashCommands.ts:174-220` | Plugin命令扫描 (`~/.claude/plugins/`) | 中等 |
| `web/src/components/NewSession/preferences.ts:6` | AgentType包含'claude' | 弱 |

#### Codex 依赖点
| 文件路径 | 依赖内容 | 依赖强度 |
|---------|---------|---------|
| `cli/src/modules/common/slashCommands.ts:35` | Codex built-in commands (空数组) | 弱 |
| `cli/src/modules/common/slashCommands.ts:92-94` | `CODEX_HOME`环境变量 | 弱 |
| `web/src/components/NewSession/preferences.ts:6` | AgentType包含'codex' | 弱 |

### 2. 会话启动依赖

`cli/src/api/apiMachine.ts:103-132` - `spawn-happy-session` RPC处理:
- 接收agent参数 (claude/codex/gemini/opencode)
- 启动对应代理进程
- **依赖强度**: 强 (核心功能)

## 上游更新合并记录

### 2025-02-11 合并 (v0.15.2)

#### 上游新增提交
1. **eb8e749** - Release version 0.15.2
- bun.lock更新
- cli/package.json版本更新

2. **b11e6ed** - feat(web): persist new session agent and yolo preferences (#171)
- 新增: `web/src/components/NewSession/preferences.ts`
- 新增: `web/src/components/NewSession/preferences.test.ts`
- 修改: `web/src/components/NewSession/index.tsx`
- **适配需求**: 无需适配,通用功能

#### 本地特有提交 (已保留)
- **cd2ae32** - feat(telegram): add telegram bot integration and settings UI

#### 冲突解决
- **文件**: `web/src/App.tsx`
- **冲突原因**: 本地分支添加了注释说明`"new"`路由过滤,upstream已包含相同修复
- **解决方案**: 保留upstream代码 + 本地注释

### 2025-02-03 前合并 (v0.15.1及之前)

已合并的上游功能:
- Slash commands插件支持 (#155)
- 路由修复: `/sessions/new` 匹配问题 (#164)
- Git状态改进
- 目录树标签页
- 内置Nerd Font支持 (#122)
- SSE重连反馈 (#125)

## 功能适配评估

### 直接可用 (无需适配)

| 功能 | 原因 |
|-----|------|
| 新会话偏好持久化 | 通用localStorage实现 |
| 目录树视图 | 通用文件系统API |
| Git状态改进 | 通用git命令调用 |
| UI/UX改进 | 与Agent无关 |
| 路由修复 | 前端路由逻辑 |

### 需要监控的变更

| 功能区域 | 监控原因 |
|---------|---------|
| `slashCommands.ts` | 新增Claude/Codex命令需同步 |
| `NewSession`组件 | 新增Agent类型需同步 |
| `preferences.ts` | Agent列表变更需同步 |
| CLI启动逻辑 | Agent启动参数变更 |

## 后续合并工作流

### 定期更新检查

```bash
# 1. 获取上游更新
git fetch upstream

# 2. 查看变更
git log --oneline --left-right upstream/main...origin/main

# 3. 分析关键文件变更
git diff upstream/main...origin/main -- cli/src/modules/common/slashCommands.ts

# 4. 合并
git merge upstream/main
```

### 重点检查清单

合并前检查以下文件是否有变更:
- [ ] `cli/src/modules/common/slashCommands.ts` - Agent命令定义
- [ ] `web/src/components/NewSession/preferences.ts` - VALID_AGENTS数组
- [ ] `web/src/types/api.ts` - Agent类型定义
- [ ] `cli/src/api/apiMachine.ts` - 会话启动参数

### 冲突处理优先级

1. **高优先级**: Agent类型相关代码
2. **中优先级**: 配置文件路径变更
3. **低优先级**: UI文本、样式

## 版本兼容性

### 当前支持版本

| Agent | 版本/配置 | 状态 |
|-------|----------|------|
| Claude | 通过`claude` CLI | 完整支持 |
| Codex | 通过`codex` CLI | 完整支持 |
| Gemini | 通过`gemini` CLI | 完整支持 |
| OpenCode | 通过`opencode` CLI | 完整支持 |

### 环境变量依赖

```bash
# Claude
CLAUDE_CONFIG_DIR=~/.claude # 可选,有默认值

# Codex
CODEX_HOME=~/.codex # 可选,有默认值
```

## 风险评估

### 低风险变更
- UI组件更新
- 样式调整
- 文档更新
- 测试文件

### 中风险变更
- 新增内置命令
- 配置文件格式变更
- API端点变更

### 高风险变更
- Agent启动协议变更
- RPC接口变更
- 数据库Schema变更

## 建议

1. **保持同步频率**: 建议每周检查一次上游更新
2. **测试策略**: 合并后在本地测试各Agent启动
3. **文档更新**: 每次合并后更新此文档的合并记录部分
4. **监控议题**: 关注upstream的breaking change通知

## 附录: 相关文件清单

### Agent相关核心文件
```
cli/src/modules/common/slashCommands.ts
cli/src/modules/common/registerCommonHandlers.ts
cli/src/api/apiMachine.ts
web/src/components/NewSession/index.tsx
web/src/components/NewSession/preferences.ts
web/src/types/api.ts
```

### 配置文件
```
CLAUDE.md (项目特定指令)
UPSTREAM_DEPENDENCIES.md (本文档)
```
1 change: 0 additions & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions cli/src/api/apiMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,9 @@ export class ApiMachineClient {
case 'success':
return { type: 'success', sessionId: result.sessionId }
case 'requestToApproveDirectoryCreation':
return { type: 'requestToApproveDirectoryCreation', directory: result.directory }
return { type: 'error', errorMessage: `Directory '${result.directory}' does not exist and needs approval to be created` }
case 'error':
throw new Error(result.errorMessage)
return { type: 'error', errorMessage: result.errorMessage }
}
})

Expand Down
4 changes: 2 additions & 2 deletions cli/src/commands/hub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ export const hubCommand: CommandDefinition = {
const { host, port } = parseHubArgs(context.commandArgs)

if (host) {
process.env.WEBAPP_HOST = host
process.env.HAPI_LISTEN_HOST = host
}
if (port) {
process.env.WEBAPP_PORT = port
process.env.HAPI_LISTEN_PORT = port
}
await import('../../../hub/src/index')
} catch (error) {
Expand Down
85 changes: 31 additions & 54 deletions cli/src/runtime/embeddedAssets.bun.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,30 @@
import { feature } from 'bun:bundle';

import difftasticArchiveLicense from '../../tools/archives/difftastic-LICENSE' assert { type: 'file' };
import ripgrepArchiveLicense from '../../tools/archives/ripgrep-LICENSE' assert { type: 'file' };
import difftasticLicense from '../../tools/licenses/difftastic-LICENSE' assert { type: 'file' };
import ripgrepLicense from '../../tools/licenses/ripgrep-LICENSE' assert { type: 'file' };
import tunwgLicense from '../../../hub/tools/tunwg/LICENSE' assert { type: 'file' };

// Platform-specific imports - all imported statically, only used based on runtime detection
import difftasticArm64Darwin from '../../tools/archives/difftastic-arm64-darwin.tar.gz' assert { type: 'file' };
import ripgrepArm64Darwin from '../../tools/archives/ripgrep-arm64-darwin.tar.gz' assert { type: 'file' };
import tunwgArm64Darwin from '../../../hub/tools/tunwg/tunwg-arm64-darwin' assert { type: 'file' };

import difftasticX64Darwin from '../../tools/archives/difftastic-x64-darwin.tar.gz' assert { type: 'file' };
import ripgrepX64Darwin from '../../tools/archives/ripgrep-x64-darwin.tar.gz' assert { type: 'file' };
import tunwgX64Darwin from '../../../hub/tools/tunwg/tunwg-x64-darwin' assert { type: 'file' };

import difftasticArm64Linux from '../../tools/archives/difftastic-arm64-linux.tar.gz' assert { type: 'file' };
import ripgrepArm64Linux from '../../tools/archives/ripgrep-arm64-linux.tar.gz' assert { type: 'file' };
import tunwgArm64Linux from '../../../hub/tools/tunwg/tunwg-arm64-linux' assert { type: 'file' };

import difftasticX64Linux from '../../tools/archives/difftastic-x64-linux.tar.gz' assert { type: 'file' };
import ripgrepX64Linux from '../../tools/archives/ripgrep-x64-linux.tar.gz' assert { type: 'file' };
import tunwgX64Linux from '../../../hub/tools/tunwg/tunwg-x64-linux' assert { type: 'file' };

import difftasticX64Win32 from '../../tools/archives/difftastic-x64-win32.tar.gz' assert { type: 'file' };
import ripgrepX64Win32 from '../../tools/archives/ripgrep-x64-win32.tar.gz' assert { type: 'file' };
import tunwgX64Win32 from '../../../hub/tools/tunwg/tunwg-x64-win32.exe' assert { type: 'file' };

export interface EmbeddedAsset {
relativePath: string;
sourcePath: string;
Expand All @@ -26,17 +45,11 @@ const COMMON_ASSETS: EmbeddedAsset[] = [
asset('tools/tunwg/LICENSE', tunwgLicense)
];

async function selectEmbeddedAssets(): Promise<EmbeddedAsset[]> {
if (feature('HAPI_TARGET_DARWIN_ARM64')) {
const [
{ default: difftasticArm64Darwin },
{ default: ripgrepArm64Darwin },
{ default: tunwgArm64Darwin }
] = await Promise.all([
import('../../tools/archives/difftastic-arm64-darwin.tar.gz', { assert: { type: 'file' } }),
import('../../tools/archives/ripgrep-arm64-darwin.tar.gz', { assert: { type: 'file' } }),
import('../../../hub/tools/tunwg/tunwg-arm64-darwin', { assert: { type: 'file' } })
]);
function selectEmbeddedAssets(): EmbeddedAsset[] {
const platform = process.platform;
const arch = process.arch;

if (platform === 'darwin' && arch === 'arm64') {
return [
...COMMON_ASSETS,
asset('tools/archives/difftastic-arm64-darwin.tar.gz', difftasticArm64Darwin),
Expand All @@ -45,16 +58,7 @@ async function selectEmbeddedAssets(): Promise<EmbeddedAsset[]> {
];
}

if (feature('HAPI_TARGET_DARWIN_X64')) {
const [
{ default: difftasticX64Darwin },
{ default: ripgrepX64Darwin },
{ default: tunwgX64Darwin }
] = await Promise.all([
import('../../tools/archives/difftastic-x64-darwin.tar.gz', { assert: { type: 'file' } }),
import('../../tools/archives/ripgrep-x64-darwin.tar.gz', { assert: { type: 'file' } }),
import('../../../hub/tools/tunwg/tunwg-x64-darwin', { assert: { type: 'file' } })
]);
if (platform === 'darwin' && arch === 'x64') {
return [
...COMMON_ASSETS,
asset('tools/archives/difftastic-x64-darwin.tar.gz', difftasticX64Darwin),
Expand All @@ -63,16 +67,7 @@ async function selectEmbeddedAssets(): Promise<EmbeddedAsset[]> {
];
}

if (feature('HAPI_TARGET_LINUX_ARM64')) {
const [
{ default: difftasticArm64Linux },
{ default: ripgrepArm64Linux },
{ default: tunwgArm64Linux }
] = await Promise.all([
import('../../tools/archives/difftastic-arm64-linux.tar.gz', { assert: { type: 'file' } }),
import('../../tools/archives/ripgrep-arm64-linux.tar.gz', { assert: { type: 'file' } }),
import('../../../hub/tools/tunwg/tunwg-arm64-linux', { assert: { type: 'file' } })
]);
if (platform === 'linux' && arch === 'arm64') {
return [
...COMMON_ASSETS,
asset('tools/archives/difftastic-arm64-linux.tar.gz', difftasticArm64Linux),
Expand All @@ -81,16 +76,7 @@ async function selectEmbeddedAssets(): Promise<EmbeddedAsset[]> {
];
}

if (feature('HAPI_TARGET_LINUX_X64')) {
const [
{ default: difftasticX64Linux },
{ default: ripgrepX64Linux },
{ default: tunwgX64Linux }
] = await Promise.all([
import('../../tools/archives/difftastic-x64-linux.tar.gz', { assert: { type: 'file' } }),
import('../../tools/archives/ripgrep-x64-linux.tar.gz', { assert: { type: 'file' } }),
import('../../../hub/tools/tunwg/tunwg-x64-linux', { assert: { type: 'file' } })
]);
if (platform === 'linux' && arch === 'x64') {
return [
...COMMON_ASSETS,
asset('tools/archives/difftastic-x64-linux.tar.gz', difftasticX64Linux),
Expand All @@ -99,16 +85,7 @@ async function selectEmbeddedAssets(): Promise<EmbeddedAsset[]> {
];
}

if (feature('HAPI_TARGET_WIN32_X64')) {
const [
{ default: difftasticX64Win32 },
{ default: ripgrepX64Win32 },
{ default: tunwgX64Win32 }
] = await Promise.all([
import('../../tools/archives/difftastic-x64-win32.tar.gz', { assert: { type: 'file' } }),
import('../../tools/archives/ripgrep-x64-win32.tar.gz', { assert: { type: 'file' } }),
import('../../../hub/tools/tunwg/tunwg-x64-win32.exe', { assert: { type: 'file' } })
]);
if (platform === 'win32' && arch === 'x64') {
return [
...COMMON_ASSETS,
asset('tools/archives/difftastic-x64-win32.tar.gz', difftasticX64Win32),
Expand All @@ -117,7 +94,7 @@ async function selectEmbeddedAssets(): Promise<EmbeddedAsset[]> {
];
}

throw new Error('No build target feature flag set. Build with --feature=HAPI_TARGET_*.');
throw new Error(`Unsupported platform: ${arch}-${platform}`);
}

export async function loadEmbeddedAssets(): Promise<EmbeddedAsset[]> {
Expand Down
Loading
Loading