farming-code 2.2.19 → 2.2.22
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/README.md +5 -5
- package/README.zh_cn.md +5 -5
- package/THIRD_PARTY_NOTICES.md +5 -8
- package/backend/acp/client-services.js +37 -5
- package/backend/acp/patch-decisions.js +19 -3
- package/backend/acp-runtime.js +1201 -198
- package/backend/acp-transcript.js +66 -0
- package/backend/agent-lifecycle-journal.js +133 -0
- package/backend/agent-manager.js +2715 -354
- package/backend/agent-order.js +8 -4
- package/backend/agent-runtime-binding.js +1 -0
- package/backend/bundle-update-helper.js +5 -1
- package/backend/config-manager.js +16 -0
- package/backend/control-api.js +146 -20
- package/backend/farming-app-cli.js +335 -10
- package/backend/farming-session-store.js +283 -36
- package/backend/json-cli-runtime.js +120 -6
- package/backend/npm-update-helper.js +97 -18
- package/backend/provider-adapters.js +18 -6
- package/backend/review-session-service.js +31 -17
- package/backend/server.js +337 -103
- package/backend/storage-layout.js +10 -0
- package/backend/terminal-screen-worker-pool.js +15 -6
- package/backend/theme-manager.js +23 -5
- package/backend/update-service.js +239 -27
- package/backend/usage-monitor.js +11 -0
- package/backend/workspace-file-router.js +9 -3
- package/backend/workspace-file-service.js +292 -62
- package/dist/acp/codex-acp-1.1.4.js +32 -1
- package/dist/assets/App-CmZdNavW.js +210 -0
- package/dist/assets/{FileEditorMarkdownPreview-R0jwwLME.js → FileEditorMarkdownPreview-KbnE8VC8.js} +1 -1
- package/dist/assets/FileEditorPane-5_HUejjd.js +2 -0
- package/dist/assets/IconGlyphs-C2WFcoJS.js +1 -0
- package/dist/assets/ProjectFilesSection-C1RAJWOE.js +12 -0
- package/dist/assets/{ReviewPage-DBKNOWsJ.js → ReviewPage-L1vh1Ggs.js} +3 -3
- package/dist/assets/code-dark-BKtvf0Xc.css +1 -0
- package/dist/assets/file-icons-CHWt5nSy.js +1 -0
- package/dist/assets/{index-DQdtPfCk.js → index-RvUiMpGP.js} +2 -2
- package/dist/assets/main-XMXgyJJt.css +1 -0
- package/dist/assets/workspace-view-state-BqB4HM7r.js +7 -0
- package/dist/index.html +1 -1
- package/frontend/skins/crt/app.js +365 -107
- package/frontend/skins/crt/index.html +5 -10
- package/frontend/skins/crt/styles/billing.css +6 -1
- package/package.json +9 -3
- package/dist/assets/App-CbXYBccJ.js +0 -210
- package/dist/assets/FileEditorPane-CJs6kfZO.js +0 -2
- package/dist/assets/IconGlyphs-CZzDr5o2.js +0 -1
- package/dist/assets/ProjectFilesSection-DWCV07lA.js +0 -12
- package/dist/assets/code-dark-qIBiLBWV.css +0 -1
- package/dist/assets/file-icons-DpbrD5ZP.js +0 -1
- package/dist/assets/main-yzZEfgft.css +0 -1
- package/dist/assets/workspace-view-state-BpPINu84.js +0 -7
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="./public/farming-2/app-icon-v2-512.png" alt="Farming" width="112">
|
|
2
|
+
<img src="./public/farming-2/app-icon-v2-512.png" alt="Farming Code" width="112">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
<h1 align="center">Farming</h1>
|
|
5
|
+
<h1 align="center">Farming Code</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
Farming is an open-source
|
|
8
|
+
Farming Code is an open-source, self-hosted browser workspace for running and supervising Codex, Claude Code, OpenCode, and other AI coding agents.
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center"><a href="./README.zh_cn.md">简体中文</a></p>
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|

|
|
23
23
|
|
|
24
|
-
Farming runs on the same development machine as your repositories and coding CLIs. Agent processes, terminals, and project files stay on that machine; a desktop or phone browser connects to those real sessions.
|
|
24
|
+
Farming Code runs on the same development machine as your repositories and coding CLIs. Agent processes, terminals, and project files stay on that machine; a desktop or phone browser connects to those real sessions.
|
|
25
25
|
|
|
26
26
|
## Quick Start
|
|
27
27
|
|
|
@@ -119,7 +119,7 @@ farming logs
|
|
|
119
119
|
farming stop
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
-
The startup token is stored in `~/.farming/.session-token` and reused across restarts and upgrades. **Settings → Updates**
|
|
122
|
+
The startup token is stored in `~/.farming/.session-token` and reused across restarts and upgrades. **Settings → Updates** uses a two-step flow for npm installations: **Prepare** installs the selected version into a separate staging directory while the current server keeps running, then **Restart to update** switches the package directory and restarts Farming only after the user clicks it. If the new server does not start, Farming restores the previous package directory. npm preparation first uses the machine's configured registry and retries the registry shown in Settings only when that registry lacks the selected version. Before preparing or applying an update, Farming verifies that npm targets the same package root that launched the server. GitHub Releases also provide standalone CLI and directory bundles; see [GitHub Releases](https://github.com/zhuwenzhuang/farming/releases) for current artifacts.
|
|
123
123
|
|
|
124
124
|
To run from source with the same port and base path:
|
|
125
125
|
|
package/README.zh_cn.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="./public/farming-2/app-icon-v2-512.png" alt="Farming" width="112">
|
|
2
|
+
<img src="./public/farming-2/app-icon-v2-512.png" alt="Farming Code" width="112">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
<h1 align="center">Farming</h1>
|
|
5
|
+
<h1 align="center">Farming Code</h1>
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
|
-
Farming
|
|
8
|
+
Farming Code 是一个开源、自托管的浏览器工作区,用于启动和管理 Codex、Claude Code、OpenCode 及其他 AI Coding Agent。
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center"><a href="./README.md">English</a></p>
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|

|
|
23
23
|
|
|
24
|
-
Farming 与代码仓库和 Coding CLI 运行在同一台开发机上。Agent 进程、终端和项目文件都留在这台机器上;电脑或手机浏览器连接并操作这些真实 Session。
|
|
24
|
+
Farming Code 与代码仓库和 Coding CLI 运行在同一台开发机上。Agent 进程、终端和项目文件都留在这台机器上;电脑或手机浏览器连接并操作这些真实 Session。
|
|
25
25
|
|
|
26
26
|
## 快速开始
|
|
27
27
|
|
|
@@ -119,7 +119,7 @@ farming logs
|
|
|
119
119
|
farming stop
|
|
120
120
|
```
|
|
121
121
|
|
|
122
|
-
启动 Token 存放在 `~/.farming/.session-token`,重启和升级会继续复用。**Settings → Updates**
|
|
122
|
+
启动 Token 存放在 `~/.farming/.session-token`,重启和升级会继续复用。**Settings → Updates** 为 npm 安装提供两阶段更新:点击**准备**后,所选版本会安装到独立 staging 目录,当前服务继续运行;准备完成后,只有用户点击**重启并应用**,Farming 才会切换包目录并重启。如果新服务启动失败,Farming 会恢复旧包目录。npm 准备阶段会先使用机器配置的 registry;只有该 registry 缺少所选版本时,才会回退到 Settings 展示版本所用的 registry。准备和应用更新前,Farming 都会校验 npm 目标包根目录与启动当前服务的包根目录一致。GitHub Releases 也提供独立 CLI 和目录 Bundle;当前产物见 [GitHub Releases](https://github.com/zhuwenzhuang/farming/releases)。
|
|
123
123
|
|
|
124
124
|
从源码运行,并使用相同的端口与 Base Path:
|
|
125
125
|
|
package/THIRD_PARTY_NOTICES.md
CHANGED
|
@@ -121,11 +121,8 @@ Microsoft, or the maintainers of the third-party projects listed above.
|
|
|
121
121
|
|
|
122
122
|
## Design References
|
|
123
123
|
|
|
124
|
-
Farming's interface
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
The CRT skin also studies the visual behavior of cool-retro-term. Farming's
|
|
130
|
-
browser shaders are an independent implementation and do not copy or bundle
|
|
131
|
-
cool-retro-term source code.
|
|
124
|
+
Farming Code's interface design references Visual Studio Code and Codex.
|
|
125
|
+
Farming's browser-based approach to terminal and coding-agent sessions also
|
|
126
|
+
draws on the idea explored by
|
|
127
|
+
[VibeTunnel](https://github.com/amantus-ai/vibetunnel). These design references
|
|
128
|
+
are separate from the software and assets identified above.
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
1
2
|
const fs = require('fs');
|
|
2
3
|
const path = require('path');
|
|
3
4
|
const { spawn } = require('child_process');
|
|
@@ -20,6 +21,10 @@ function requireMatchingSession(binding, params) {
|
|
|
20
21
|
}
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
function requireOpenBinding(binding) {
|
|
25
|
+
if (binding?.exited === true) throw new Error('ACP Agent connection is closed');
|
|
26
|
+
}
|
|
27
|
+
|
|
23
28
|
async function resolveWorkspacePath(binding, requestedPath, options = {}) {
|
|
24
29
|
const value = String(requestedPath || '');
|
|
25
30
|
if (!path.isAbsolute(value)) throw new Error('ACP file and terminal paths must be absolute');
|
|
@@ -76,6 +81,7 @@ class AcpClientFileSystem {
|
|
|
76
81
|
throw new Error('ACP text file is too large to write');
|
|
77
82
|
}
|
|
78
83
|
const target = await resolveWorkspacePath(binding, params.path, { allowMissing: true });
|
|
84
|
+
requireOpenBinding(binding);
|
|
79
85
|
let mode = 0o666;
|
|
80
86
|
try {
|
|
81
87
|
const existing = await fsp.realpath(target);
|
|
@@ -87,9 +93,14 @@ class AcpClientFileSystem {
|
|
|
87
93
|
} catch (error) {
|
|
88
94
|
if (error?.code !== 'ENOENT') throw error;
|
|
89
95
|
}
|
|
90
|
-
const temporary = path.join(
|
|
96
|
+
const temporary = path.join(
|
|
97
|
+
path.dirname(target),
|
|
98
|
+
`.${path.basename(target)}.farming-acp-${process.pid}-${crypto.randomUUID()}.tmp`,
|
|
99
|
+
);
|
|
91
100
|
try {
|
|
101
|
+
requireOpenBinding(binding);
|
|
92
102
|
await fsp.writeFile(temporary, content, { mode });
|
|
103
|
+
requireOpenBinding(binding);
|
|
93
104
|
await fsp.rename(temporary, target);
|
|
94
105
|
} catch (error) {
|
|
95
106
|
await fsp.rm(temporary, { force: true }).catch(() => {});
|
|
@@ -120,8 +131,8 @@ class AcpClientTerminalManager {
|
|
|
120
131
|
: typeof options.spawn === 'function'
|
|
121
132
|
? null
|
|
122
133
|
: nodePty.spawn;
|
|
123
|
-
this.sequence = 0;
|
|
124
134
|
this.terminals = new Map();
|
|
135
|
+
this.pendingCreates = new Map();
|
|
125
136
|
}
|
|
126
137
|
|
|
127
138
|
activeCount(agentId) {
|
|
@@ -132,23 +143,44 @@ class AcpClientTerminalManager {
|
|
|
132
143
|
return count;
|
|
133
144
|
}
|
|
134
145
|
|
|
146
|
+
reserveCreate(agentId) {
|
|
147
|
+
const pending = this.pendingCreates.get(agentId) || 0;
|
|
148
|
+
if (this.activeCount(agentId) + pending >= MAX_ACTIVE_TERMINALS_PER_AGENT) {
|
|
149
|
+
throw new Error('ACP terminal limit reached for this Agent');
|
|
150
|
+
}
|
|
151
|
+
this.pendingCreates.set(agentId, pending + 1);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
releaseCreate(agentId) {
|
|
155
|
+
const pending = this.pendingCreates.get(agentId) || 0;
|
|
156
|
+
if (pending <= 1) this.pendingCreates.delete(agentId);
|
|
157
|
+
else this.pendingCreates.set(agentId, pending - 1);
|
|
158
|
+
}
|
|
159
|
+
|
|
135
160
|
async create(binding, params) {
|
|
136
161
|
requireMatchingSession(binding, params);
|
|
137
|
-
|
|
138
|
-
|
|
162
|
+
this.reserveCreate(binding.agentId);
|
|
163
|
+
try {
|
|
164
|
+
return await this.createReserved(binding, params);
|
|
165
|
+
} finally {
|
|
166
|
+
this.releaseCreate(binding.agentId);
|
|
139
167
|
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async createReserved(binding, params) {
|
|
140
171
|
const command = String(params.command || '').trim();
|
|
141
172
|
if (!command) throw new Error('ACP terminal command is required');
|
|
142
173
|
const cwd = params.cwd
|
|
143
174
|
? await resolveWorkspacePath(binding, params.cwd)
|
|
144
175
|
: await fsp.realpath(binding.cwd);
|
|
176
|
+
requireOpenBinding(binding);
|
|
145
177
|
const env = { ...binding.env };
|
|
146
178
|
for (const item of Array.isArray(params.env) ? params.env : []) {
|
|
147
179
|
const name = String(item?.name || '');
|
|
148
180
|
if (!name || name.includes('=') || name.includes('\0')) throw new Error('Invalid ACP terminal environment variable');
|
|
149
181
|
env[name] = String(item?.value ?? '');
|
|
150
182
|
}
|
|
151
|
-
const terminalId = `acp-terminal-${
|
|
183
|
+
const terminalId = `acp-terminal-${crypto.randomUUID()}`;
|
|
152
184
|
const args = Array.isArray(params.args) ? params.args.map(String) : [];
|
|
153
185
|
const child = this.ptySpawn
|
|
154
186
|
? this.ptySpawn(command, args, {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
1
2
|
const fs = require('fs');
|
|
2
3
|
const path = require('path');
|
|
3
4
|
const { resolveWorkspacePath } = require('./client-services');
|
|
@@ -91,7 +92,10 @@ async function missingTarget(root, requestedPath) {
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
async function atomicWrite(target, content, mode = 0o666) {
|
|
94
|
-
const temporary = path.join(
|
|
95
|
+
const temporary = path.join(
|
|
96
|
+
path.dirname(target),
|
|
97
|
+
`.${path.basename(target)}.farming-acp-revert-${process.pid}-${crypto.randomUUID()}.tmp`,
|
|
98
|
+
);
|
|
95
99
|
try {
|
|
96
100
|
await fsp.writeFile(temporary, content, { mode });
|
|
97
101
|
await fsp.rename(temporary, target);
|
|
@@ -111,14 +115,26 @@ async function rejectPatch({ entry, root, requestedPath }) {
|
|
|
111
115
|
const current = await existingTarget(root, requestedPath);
|
|
112
116
|
|
|
113
117
|
if (deleted) {
|
|
114
|
-
if (current.exists)
|
|
118
|
+
if (current.exists) {
|
|
119
|
+
const currentText = await fsp.readFile(current.target, 'utf8');
|
|
120
|
+
if (currentText === oldText) {
|
|
121
|
+
return { action: 'reverted', path: relativeWorkspacePath(root, current.logical) };
|
|
122
|
+
}
|
|
123
|
+
throw new AcpPatchDecisionError('File changed after this ACP patch; it was not reverted', 409);
|
|
124
|
+
}
|
|
115
125
|
const target = await missingTarget(root, requestedPath);
|
|
116
126
|
await atomicWrite(target.target, oldText);
|
|
117
127
|
return { action: 'reverted', path: relativeWorkspacePath(root, target.logical) };
|
|
118
128
|
}
|
|
119
129
|
|
|
120
|
-
if (!current.exists)
|
|
130
|
+
if (!current.exists) {
|
|
131
|
+
if (added) return { action: 'reverted', path: relativeWorkspacePath(root, current.logical) };
|
|
132
|
+
throw new AcpPatchDecisionError('File changed after this ACP patch; it was not reverted', 409);
|
|
133
|
+
}
|
|
121
134
|
const currentText = await fsp.readFile(current.target, 'utf8');
|
|
135
|
+
if (!added && currentText === oldText) {
|
|
136
|
+
return { action: 'reverted', path: relativeWorkspacePath(root, current.logical) };
|
|
137
|
+
}
|
|
122
138
|
if (currentText !== newText) throw new AcpPatchDecisionError('File changed after this ACP patch; it was not reverted', 409);
|
|
123
139
|
|
|
124
140
|
if (added) {
|