farming-code 2.2.7 → 2.2.11
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 +110 -199
- package/README.zh_cn.md +106 -237
- package/THIRD_PARTY_NOTICES.md +11 -2
- package/backend/acp-checkpoint-store.js +198 -0
- package/backend/acp-runtime.js +306 -83
- package/backend/acp-session-state.js +202 -6
- package/backend/acp-transcript.js +112 -0
- package/backend/agent-activity.js +6 -157
- package/backend/agent-manager.js +1496 -737
- package/backend/agent-provider-session.js +17 -242
- package/backend/agent-runtime-binding.js +219 -0
- package/backend/agent-session-history.js +66 -1
- package/backend/auth.js +79 -6
- package/backend/codex-models.js +81 -84
- package/backend/codex-session-archive.js +45 -0
- package/backend/codex-terminal-profile.js +500 -0
- package/backend/codex-transcript-sanitizer.js +12 -0
- package/backend/codex-transcript.js +230 -8
- package/backend/config-manager.js +30 -0
- package/backend/control-api.js +192 -17
- package/backend/farming-net-pass.js +285 -0
- package/backend/farming-net-registry.js +112 -0
- package/backend/farming-net-server.js +298 -0
- package/backend/farming-session-store.js +5 -13
- package/backend/git-worktree-info.js +181 -0
- package/backend/local-session-engine.js +411 -186
- package/backend/main-page-session.js +5 -2
- package/backend/native-pty-controller-generation.js +106 -0
- package/backend/native-pty-host-client.js +275 -7
- package/backend/native-pty-host-identity.js +86 -0
- package/backend/native-pty-host.js +813 -114
- package/backend/native-session-engine.js +100 -28
- package/backend/packaged-node-pty.js +22 -2
- package/backend/provider-adapters.js +253 -0
- package/backend/provider-session-service.js +241 -0
- package/backend/runtime-observation.js +81 -0
- package/backend/server.js +294 -86
- package/backend/session-engine-bridge.js +21 -2
- package/backend/session-engine-router.js +1 -1
- package/backend/session-engine.js +1 -1
- package/backend/session-stream-protocol.js +185 -0
- package/backend/storage-layout.js +55 -0
- package/backend/terminal-attach-checkpoint.js +74 -0
- package/backend/terminal-exit-quiescence.js +39 -0
- package/backend/terminal-reducer-flow-control.js +97 -0
- package/backend/terminal-screen-state.js +11 -2
- package/backend/terminal-screen-worker-pool.js +59 -6
- package/backend/terminal-screen-worker-thread.js +97 -57
- package/backend/terminal-screen-worker.js +133 -51
- package/backend/terminal-state-serialization.js +127 -0
- package/backend/terminal-status.js +23 -4
- package/backend/usage-monitor.js +81 -4
- package/backend/workspace-directory.js +152 -0
- package/backend/workspace-file-router.js +182 -76
- package/backend/workspace-file-service.js +319 -4
- package/backend/workspace-root-registry.js +164 -0
- package/dist/assets/App-CHYpgECZ.js +208 -0
- package/dist/assets/{FileEditorMarkdownPreview-elKWc8Im.js → FileEditorMarkdownPreview--Cri80nQ.js} +92 -92
- package/dist/assets/FileEditorPane-DGJlEKdf.js +2 -0
- package/dist/assets/IconGlyphs-Cc14sepw.js +1 -0
- package/dist/assets/ProjectFilesSection-Cz1wG4NT.js +12 -0
- package/dist/assets/{ReviewPage-BaXu1ZdX.js → ReviewPage-LonRc6Fz.js} +1 -1
- package/dist/assets/code-dark-B1pAtO_P.css +1 -0
- package/dist/assets/file-icons-Bw2qd5iT.js +1 -0
- package/dist/assets/{index-B8SHldPE.js → index-miO2r9IZ.js} +3 -3
- package/dist/assets/main-DNe0jPw-.css +1 -0
- package/dist/assets/workspace-editor-model-BQol4qbA.js +1 -0
- package/dist/assets/workspace-editor-monaco-5gFbeDPF.js +1 -0
- package/dist/assets/workspace-editor-monaco-xPv8JzHH.js +4 -0
- package/dist/assets/workspace-view-state-DvYG_9PH.js +7 -0
- package/dist/assets/workspace-working-copy-D8-s_Sgh.js +1 -0
- package/dist/index.html +1 -1
- package/frontend/farming-net/app.css +625 -0
- package/frontend/farming-net/app.js +268 -0
- package/frontend/farming-net/index.html +86 -0
- package/frontend/reading-anchor.js +198 -0
- package/frontend/session-bridge.js +12 -3
- package/frontend/session-modal-bridge.js +5 -12
- package/frontend/skins/crt/app.js +1976 -793
- package/frontend/skins/crt/index.html +313 -23
- package/frontend/skins/crt/styles/billing.css +294 -223
- package/frontend/skins/crt/styles/monochrome-green.css +7 -2
- package/frontend/terminal-replay.js +372 -0
- package/package.json +10 -3
- package/shared/browser-protocol.d.ts +5 -0
- package/shared/browser-protocol.js +130 -0
- package/dist/assets/App-iA2BU9P6.js +0 -124
- package/dist/assets/FileEditorPane-RWiFD2cq.js +0 -5
- package/dist/assets/IconGlyphs-DfL0EBnj.js +0 -1
- package/dist/assets/ProjectFilesSection-Q4PDsWmM.js +0 -12
- package/dist/assets/code-dark-CDkOQAtK.css +0 -1
- package/dist/assets/file-icons-EFUGSSwf.js +0 -1
- package/dist/assets/main-DpYuBonn.css +0 -1
- package/dist/assets/qoder-C9LmmOSf.svg +0 -1
- package/dist/assets/qoder-Cf9gl0Y5.svg +0 -1
- package/dist/assets/qoder-gHCinseV.svg +0 -1
- package/dist/assets/workspace-view-state-CTyDzk2D.js +0 -1
- package/dist/assets/zsh-CLpveKlF.svg +0 -1
- package/dist/assets/zsh-FxSpMPbz.svg +0 -1
- /package/dist/assets/{api-D1lyBYIQ.js → api-D8nyOEbz.js} +0 -0
- /package/dist/assets/{core-ZlAPicox.js → core-D0LFJkDt.js} +0 -0
- /package/dist/assets/{useWorkspaceMenuKeyboard-CneKAZUJ.js → useWorkspaceMenuKeyboard-Brws6Ar9.js} +0 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const http = require('http');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const TokenAuth = require('./auth');
|
|
8
|
+
const {
|
|
9
|
+
createFarmingNetPass,
|
|
10
|
+
loadOrCreateFarmingNetSigningIdentity,
|
|
11
|
+
PASS_QUERY_PARAM,
|
|
12
|
+
} = require('./farming-net-pass');
|
|
13
|
+
const { loadFarmingNetRegistry } = require('./farming-net-registry');
|
|
14
|
+
const storageLayout = require('./storage-layout');
|
|
15
|
+
|
|
16
|
+
const DEFAULT_PORT = 6693;
|
|
17
|
+
const DEFAULT_BASE_PATH = '/farming-net';
|
|
18
|
+
|
|
19
|
+
function normalizeBasePath(basePath) {
|
|
20
|
+
const value = String(basePath || '').trim();
|
|
21
|
+
if (!value || value === '/') return '';
|
|
22
|
+
const normalized = value.startsWith('/') ? value : `/${value}`;
|
|
23
|
+
return normalized.endsWith('/') ? normalized.slice(0, -1) : normalized;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isTruthy(value) {
|
|
27
|
+
return ['1', 'true', 'yes', 'on'].includes(String(value || '').toLowerCase());
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function safePort(value, fallback = DEFAULT_PORT) {
|
|
31
|
+
const port = Number(value);
|
|
32
|
+
return Number.isInteger(port) && port > 0 && port <= 65535 ? port : fallback;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function loadPackageVersion(projectRoot) {
|
|
36
|
+
try {
|
|
37
|
+
return String(JSON.parse(fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8')).version || 'dev');
|
|
38
|
+
} catch {
|
|
39
|
+
return 'dev';
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function setSecurityHeaders(res) {
|
|
44
|
+
res.setHeader('Content-Security-Policy', [
|
|
45
|
+
"default-src 'self'",
|
|
46
|
+
"connect-src 'self' http: https:",
|
|
47
|
+
"img-src 'self' data:",
|
|
48
|
+
"style-src 'self'",
|
|
49
|
+
"script-src 'self'",
|
|
50
|
+
"base-uri 'none'",
|
|
51
|
+
"frame-ancestors 'none'",
|
|
52
|
+
"form-action 'self'",
|
|
53
|
+
].join('; '));
|
|
54
|
+
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin-allow-popups');
|
|
55
|
+
res.setHeader('Referrer-Policy', 'no-referrer');
|
|
56
|
+
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
57
|
+
res.setHeader('X-Frame-Options', 'DENY');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function sendBuffer(req, res, statusCode, body, contentType, cacheControl = 'no-store') {
|
|
61
|
+
const buffer = Buffer.isBuffer(body) ? body : Buffer.from(String(body));
|
|
62
|
+
res.writeHead(statusCode, {
|
|
63
|
+
'Cache-Control': cacheControl,
|
|
64
|
+
'Content-Length': buffer.length,
|
|
65
|
+
'Content-Type': contentType,
|
|
66
|
+
});
|
|
67
|
+
if (req.method === 'HEAD') res.end();
|
|
68
|
+
else res.end(buffer);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function sendJson(req, res, statusCode, payload) {
|
|
72
|
+
sendBuffer(req, res, statusCode, `${JSON.stringify(payload)}\n`, 'application/json; charset=utf-8');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function readAsset(filePath) {
|
|
76
|
+
try {
|
|
77
|
+
return fs.readFileSync(filePath);
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function browserRegistry(registry, basePath) {
|
|
84
|
+
return {
|
|
85
|
+
...registry,
|
|
86
|
+
instances: registry.instances.map(instance => ({
|
|
87
|
+
...instance,
|
|
88
|
+
endpoints: instance.endpoints.map((endpoint, index) => ({
|
|
89
|
+
...endpoint,
|
|
90
|
+
launchUrl: instance.federated
|
|
91
|
+
? `${basePath}/open/${encodeURIComponent(instance.id)}/${index}`
|
|
92
|
+
: endpoint.url,
|
|
93
|
+
})),
|
|
94
|
+
})),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function createFarmingNetServer(options = {}) {
|
|
99
|
+
const env = options.env || process.env;
|
|
100
|
+
const projectRoot = options.projectRoot || path.resolve(__dirname, '..');
|
|
101
|
+
const basePath = normalizeBasePath(options.basePath ?? env.FARMING_NET_BASE_PATH ?? DEFAULT_BASE_PATH);
|
|
102
|
+
const configDir = options.configDir
|
|
103
|
+
|| env.FARMING_NET_CONFIG_DIR
|
|
104
|
+
|| path.join(env.HOME || os.homedir(), '.farming-net');
|
|
105
|
+
const assetDir = options.assetDir || path.join(projectRoot, 'frontend', 'farming-net');
|
|
106
|
+
const registryFile = options.registryFile || storageLayout.farmingNetInstancesFile(configDir);
|
|
107
|
+
const stateFile = options.stateFile || storageLayout.farmingNetServerStateFile(configDir);
|
|
108
|
+
const packageVersion = options.packageVersion || loadPackageVersion(projectRoot);
|
|
109
|
+
const iconCandidates = [
|
|
110
|
+
options.iconFile,
|
|
111
|
+
path.join(projectRoot, 'public', 'farming-2', 'app-icon-v2-180.png'),
|
|
112
|
+
path.join(projectRoot, 'dist', 'farming-2', 'app-icon-v2-180.png'),
|
|
113
|
+
].filter(Boolean);
|
|
114
|
+
const iconFile = iconCandidates.find(candidate => fs.existsSync(candidate)) || '';
|
|
115
|
+
fs.mkdirSync(configDir, { recursive: true });
|
|
116
|
+
loadFarmingNetRegistry(registryFile);
|
|
117
|
+
const signingIdentity = options.signingIdentity || loadOrCreateFarmingNetSigningIdentity({
|
|
118
|
+
privateKeyFile: storageLayout.farmingNetSigningPrivateKeyFile(configDir),
|
|
119
|
+
publicKeyFile: storageLayout.farmingNetSigningPublicKeyFile(configDir),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const authEnv = {
|
|
123
|
+
...env,
|
|
124
|
+
FARMING_DISABLE_AUTH: env.FARMING_NET_DISABLE_AUTH || '',
|
|
125
|
+
FARMING_TOKEN: env.FARMING_NET_TOKEN || '',
|
|
126
|
+
};
|
|
127
|
+
const tokenAuth = options.tokenAuth || new TokenAuth({
|
|
128
|
+
basePath: basePath || '/',
|
|
129
|
+
cookieName: 'farming_net_token',
|
|
130
|
+
cookiePath: basePath || '/',
|
|
131
|
+
disabled: options.authDisabled === true || isTruthy(env.FARMING_NET_DISABLE_AUTH),
|
|
132
|
+
env: authEnv,
|
|
133
|
+
farmingDir: configDir,
|
|
134
|
+
farmingNetPassVerifier: false,
|
|
135
|
+
redirectQueryToken: true,
|
|
136
|
+
token: env.FARMING_NET_TOKEN || '',
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const routePath = suffix => `${basePath}${suffix}` || '/';
|
|
140
|
+
const staticAssets = new Map([
|
|
141
|
+
[routePath('/app.css'), { file: path.join(assetDir, 'app.css'), type: 'text/css; charset=utf-8' }],
|
|
142
|
+
[routePath('/app.js'), { file: path.join(assetDir, 'app.js'), type: 'text/javascript; charset=utf-8' }],
|
|
143
|
+
[routePath('/icon.png'), { file: iconFile, type: 'image/png' }],
|
|
144
|
+
]);
|
|
145
|
+
|
|
146
|
+
const server = http.createServer((req, res) => {
|
|
147
|
+
setSecurityHeaders(res);
|
|
148
|
+
tokenAuth.middleware()(req, res, () => {
|
|
149
|
+
try {
|
|
150
|
+
const url = new URL(req.url || '/', `http://${req.headers.host || 'localhost'}`);
|
|
151
|
+
const pathname = url.pathname;
|
|
152
|
+
if (!['GET', 'HEAD'].includes(String(req.method || 'GET').toUpperCase())) {
|
|
153
|
+
sendJson(req, res, 405, { error: 'Method not allowed' });
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (pathname === routePath('/api/auth/status')) {
|
|
158
|
+
sendJson(req, res, 200, { authRequired: tokenAuth.isEnabled() });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
if (pathname === routePath('/api/status')) {
|
|
162
|
+
sendJson(req, res, 200, {
|
|
163
|
+
name: 'Farming Net',
|
|
164
|
+
version: packageVersion,
|
|
165
|
+
basePath: basePath || '/',
|
|
166
|
+
});
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (pathname === routePath('/api/instances')) {
|
|
170
|
+
sendJson(req, res, 200, browserRegistry(loadFarmingNetRegistry(registryFile), basePath));
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
const openPrefix = routePath('/open/');
|
|
174
|
+
if (pathname.startsWith(openPrefix)) {
|
|
175
|
+
const parts = pathname.slice(openPrefix.length).split('/');
|
|
176
|
+
const instanceId = parts[0] || '';
|
|
177
|
+
const endpointIndex = Number(parts[1]);
|
|
178
|
+
const registry = loadFarmingNetRegistry(registryFile);
|
|
179
|
+
const instance = registry.instances.find(item => item.id === instanceId);
|
|
180
|
+
const endpoint = Number.isInteger(endpointIndex) && endpointIndex >= 0
|
|
181
|
+
? instance && instance.endpoints[endpointIndex]
|
|
182
|
+
: null;
|
|
183
|
+
if (!instance || !endpoint || parts.length !== 2) {
|
|
184
|
+
sendJson(req, res, 404, { error: 'Farming instance endpoint not found' });
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const target = new URL(endpoint.url);
|
|
188
|
+
if (instance.federated) {
|
|
189
|
+
target.searchParams.set(PASS_QUERY_PARAM, createFarmingNetPass(signingIdentity, {
|
|
190
|
+
audience: instance.id,
|
|
191
|
+
subject: 'owner',
|
|
192
|
+
ttlSeconds: env.FARMING_NET_PASS_TTL_SECONDS,
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
res.writeHead(302, {
|
|
196
|
+
'Cache-Control': 'no-store',
|
|
197
|
+
Location: target.toString(),
|
|
198
|
+
});
|
|
199
|
+
res.end();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (basePath && pathname === basePath) {
|
|
203
|
+
res.writeHead(302, { Location: `${basePath || ''}/` });
|
|
204
|
+
res.end();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (pathname === (basePath ? `${basePath}/` : '/')) {
|
|
208
|
+
const index = readAsset(path.join(assetDir, 'index.html'));
|
|
209
|
+
if (!index) {
|
|
210
|
+
sendJson(req, res, 500, { error: 'Farming Net frontend assets are missing' });
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
sendBuffer(req, res, 200, index, 'text/html; charset=utf-8');
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const asset = staticAssets.get(pathname);
|
|
218
|
+
if (asset && asset.file) {
|
|
219
|
+
const body = readAsset(asset.file);
|
|
220
|
+
if (body) {
|
|
221
|
+
sendBuffer(req, res, 200, body, asset.type, 'public, max-age=3600');
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
sendJson(req, res, 404, { error: 'Not found' });
|
|
226
|
+
} catch (error) {
|
|
227
|
+
console.error('Farming Net request failed:', error);
|
|
228
|
+
if (!res.headersSent) sendJson(req, res, 500, { error: 'Farming Net request failed' });
|
|
229
|
+
else res.end();
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
server.on('listening', () => {
|
|
235
|
+
const address = server.address();
|
|
236
|
+
const listeningPort = address && typeof address === 'object' ? address.port : null;
|
|
237
|
+
fs.writeFileSync(stateFile, `${JSON.stringify({
|
|
238
|
+
pid: process.pid,
|
|
239
|
+
port: listeningPort,
|
|
240
|
+
basePath: basePath || '/',
|
|
241
|
+
configDir,
|
|
242
|
+
registryFile,
|
|
243
|
+
signingIssuer: signingIdentity.issuer,
|
|
244
|
+
updatedAt: new Date().toISOString(),
|
|
245
|
+
}, null, 2)}\n`, { mode: 0o600 });
|
|
246
|
+
});
|
|
247
|
+
server.on('close', () => {
|
|
248
|
+
try {
|
|
249
|
+
const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'));
|
|
250
|
+
if (state.pid === process.pid) fs.unlinkSync(stateFile);
|
|
251
|
+
} catch {
|
|
252
|
+
// Ignore missing or stale state files.
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
basePath,
|
|
258
|
+
configDir,
|
|
259
|
+
packageVersion,
|
|
260
|
+
registryFile,
|
|
261
|
+
server,
|
|
262
|
+
stateFile,
|
|
263
|
+
signingIdentity,
|
|
264
|
+
tokenAuth,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function startFromCommandLine() {
|
|
269
|
+
const service = createFarmingNetServer();
|
|
270
|
+
const port = safePort(process.env.FARMING_NET_PORT || process.env.PORT, DEFAULT_PORT);
|
|
271
|
+
const host = process.env.FARMING_NET_HOST || '0.0.0.0';
|
|
272
|
+
service.server.listen(port, host, () => {
|
|
273
|
+
const entryPath = `${service.basePath || ''}/`;
|
|
274
|
+
const token = service.tokenAuth.getToken();
|
|
275
|
+
const query = token ? `?token=${encodeURIComponent(token)}` : '';
|
|
276
|
+
console.log(`Farming Net ${service.packageVersion}`);
|
|
277
|
+
console.log(`URL: http://127.0.0.1:${port}${entryPath}${query}`);
|
|
278
|
+
console.log(`Registry: ${service.registryFile}`);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
const shutdown = signal => {
|
|
282
|
+
console.log(`Farming Net received ${signal}; shutting down.`);
|
|
283
|
+
service.server.close(() => process.exit(0));
|
|
284
|
+
setTimeout(() => process.exit(1), 5000).unref();
|
|
285
|
+
};
|
|
286
|
+
process.once('SIGINT', () => shutdown('SIGINT'));
|
|
287
|
+
process.once('SIGTERM', () => shutdown('SIGTERM'));
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (require.main === module) startFromCommandLine();
|
|
291
|
+
|
|
292
|
+
module.exports = {
|
|
293
|
+
DEFAULT_BASE_PATH,
|
|
294
|
+
DEFAULT_PORT,
|
|
295
|
+
createFarmingNetServer,
|
|
296
|
+
normalizeBasePath,
|
|
297
|
+
safePort,
|
|
298
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const crypto = require('crypto');
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
|
+
const { legacyRuntimeMetadata } = require('./agent-runtime-binding');
|
|
4
5
|
const storageLayout = require('./storage-layout');
|
|
5
6
|
|
|
6
7
|
const SESSION_ID_PREFIX = 'fsess';
|
|
@@ -166,24 +167,14 @@ class FarmingSessionStore {
|
|
|
166
167
|
providerSessionSource: typeof agent.providerSessionSource === 'string' ? agent.providerSessionSource : '',
|
|
167
168
|
providerSessionResolvedAt: typeof agent.providerSessionResolvedAt === 'number' ? agent.providerSessionResolvedAt : null,
|
|
168
169
|
providerSessionTitle: typeof agent.providerSessionTitle === 'string' ? agent.providerSessionTitle : '',
|
|
170
|
+
providerSessionWorkspace: typeof agent.providerSessionWorkspace === 'string' ? agent.providerSessionWorkspace : '',
|
|
169
171
|
terminalInputReceived: agent.terminalInputReceived === true,
|
|
170
|
-
|
|
171
|
-
agentRuntimeMode: typeof agent.agentRuntimeMode === 'string' ? agent.agentRuntimeMode : 'terminal',
|
|
172
|
-
acpState: typeof agent.acpState === 'string' ? agent.acpState : '',
|
|
173
|
-
acpError: typeof agent.acpError === 'string' ? agent.acpError : '',
|
|
174
|
-
acpStopReason: typeof agent.acpStopReason === 'string' ? agent.acpStopReason : '',
|
|
175
|
-
acpSessionUpdatedAt: typeof agent.acpSessionUpdatedAt === 'string' ? agent.acpSessionUpdatedAt : '',
|
|
176
|
-
codexAppServerHomePath: typeof agent.codexAppServerHomePath === 'string' ? agent.codexAppServerHomePath : '',
|
|
177
|
-
codexAppServerState: typeof agent.codexAppServerState === 'string' ? agent.codexAppServerState : '',
|
|
178
|
-
codexAppServerEndpoint: typeof agent.codexAppServerEndpoint === 'string' ? agent.codexAppServerEndpoint : '',
|
|
179
|
-
codexAppServerThreadId: typeof agent.codexAppServerThreadId === 'string' ? agent.codexAppServerThreadId : '',
|
|
180
|
-
codexAppServerTurnId: typeof agent.codexAppServerTurnId === 'string' ? agent.codexAppServerTurnId : '',
|
|
181
|
-
codexAppServerError: typeof agent.codexAppServerError === 'string' ? agent.codexAppServerError : '',
|
|
182
|
-
codexCliObserverDeferred: agent.codexCliObserverDeferred === true,
|
|
172
|
+
...legacyRuntimeMetadata(agent),
|
|
183
173
|
engine: typeof agent.engineName === 'string' ? agent.engineName : '',
|
|
184
174
|
category: typeof agent.category === 'string' ? agent.category : '',
|
|
185
175
|
task: typeof agent.task === 'string' ? agent.task : '',
|
|
186
176
|
workflowTemplate: typeof agent.workflowTemplate === 'string' ? agent.workflowTemplate : '',
|
|
177
|
+
wantsMain: agent.wantsMain === true,
|
|
187
178
|
pinned: agent.pinned === true,
|
|
188
179
|
projectOrder: typeof agent.projectOrder === 'number' && Number.isFinite(agent.projectOrder) ? agent.projectOrder : null,
|
|
189
180
|
pinnedOrder: typeof agent.pinnedOrder === 'number' && Number.isFinite(agent.pinnedOrder) ? agent.pinnedOrder : null,
|
|
@@ -195,6 +186,7 @@ class FarmingSessionStore {
|
|
|
195
186
|
attentionOutputSeq: typeof agent.attentionOutputSeq === 'number' ? agent.attentionOutputSeq : null,
|
|
196
187
|
archived: agent.archived === true,
|
|
197
188
|
archivedAt: typeof agent.archivedAt === 'number' ? agent.archivedAt : null,
|
|
189
|
+
customTitle: typeof agent.customTitle === 'string' ? agent.customTitle : '',
|
|
198
190
|
title: typeof agent.customTitle === 'string' && agent.customTitle
|
|
199
191
|
? agent.customTitle
|
|
200
192
|
: (typeof agent.providerSessionTitle === 'string' && agent.providerSessionTitle
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
const { execFile } = require('child_process');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { promisify } = require('util');
|
|
4
|
+
|
|
5
|
+
const execFileAsync = promisify(execFile);
|
|
6
|
+
const DEFAULT_TIMEOUT_MS = 5000;
|
|
7
|
+
const DEFAULT_CACHE_MS = 3000;
|
|
8
|
+
const cache = new Map();
|
|
9
|
+
|
|
10
|
+
function normalizePathValue(value) {
|
|
11
|
+
if (typeof value !== 'string') return '';
|
|
12
|
+
const trimmed = value.trim();
|
|
13
|
+
if (!trimmed) return '';
|
|
14
|
+
return path.resolve(trimmed);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isSameOrDescendantPath(root, target) {
|
|
18
|
+
const relative = path.relative(root, target);
|
|
19
|
+
return relative === '' || Boolean(relative) && !relative.startsWith('..') && !path.isAbsolute(relative);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function normalizeBranchRef(value) {
|
|
23
|
+
const branch = String(value || '').trim();
|
|
24
|
+
return branch.startsWith('refs/heads/') ? branch.slice('refs/heads/'.length) : branch;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseGitWorktreeList(output) {
|
|
28
|
+
const records = [];
|
|
29
|
+
let current = null;
|
|
30
|
+
|
|
31
|
+
for (const token of String(output || '').split('\0')) {
|
|
32
|
+
if (!token) {
|
|
33
|
+
if (current) records.push(current);
|
|
34
|
+
current = null;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const separator = token.indexOf(' ');
|
|
39
|
+
const key = separator === -1 ? token : token.slice(0, separator);
|
|
40
|
+
const value = separator === -1 ? '' : token.slice(separator + 1);
|
|
41
|
+
if (key === 'worktree') {
|
|
42
|
+
if (current) records.push(current);
|
|
43
|
+
current = { path: normalizePathValue(value) };
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (!current) continue;
|
|
47
|
+
|
|
48
|
+
if (key === 'HEAD') current.head = value;
|
|
49
|
+
if (key === 'branch') current.branch = normalizeBranchRef(value);
|
|
50
|
+
if (key === 'bare') current.bare = true;
|
|
51
|
+
if (key === 'detached') current.detached = true;
|
|
52
|
+
if (key === 'locked') {
|
|
53
|
+
current.locked = true;
|
|
54
|
+
current.lockReason = value || '';
|
|
55
|
+
}
|
|
56
|
+
if (key === 'prunable') {
|
|
57
|
+
current.prunable = true;
|
|
58
|
+
current.pruneReason = value || '';
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (current) records.push(current);
|
|
63
|
+
return records.filter(record => record.path);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function matchingWorktree(records, target) {
|
|
67
|
+
return records
|
|
68
|
+
.filter(record => isSameOrDescendantPath(record.path, target))
|
|
69
|
+
.sort((a, b) => b.path.length - a.path.length)[0] || null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function inspectGitWorktreeUncached(workspace, options = {}) {
|
|
73
|
+
const candidate = normalizePathValue(workspace);
|
|
74
|
+
if (!candidate) return null;
|
|
75
|
+
const timeout = Number.isFinite(options.timeoutMs)
|
|
76
|
+
? Math.max(250, options.timeoutMs)
|
|
77
|
+
: DEFAULT_TIMEOUT_MS;
|
|
78
|
+
const exec = options.execFileAsync || execFileAsync;
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const commonDirPromise = exec('git', ['-C', candidate, 'rev-parse', '--path-format=absolute', '--git-common-dir'], {
|
|
82
|
+
timeout,
|
|
83
|
+
maxBuffer: 1024 * 1024,
|
|
84
|
+
}).catch(async () => {
|
|
85
|
+
const { stdout } = await exec('git', ['-C', candidate, 'rev-parse', '--git-common-dir'], {
|
|
86
|
+
timeout,
|
|
87
|
+
maxBuffer: 1024 * 1024,
|
|
88
|
+
});
|
|
89
|
+
const value = String(stdout || '').trim();
|
|
90
|
+
return { stdout: path.isAbsolute(value) ? value : path.resolve(candidate, value) };
|
|
91
|
+
});
|
|
92
|
+
const [{ stdout: topLevelOutput }, { stdout: commonDirOutput }, { stdout: listOutput }] = await Promise.all([
|
|
93
|
+
exec('git', ['-C', candidate, 'rev-parse', '--show-toplevel'], {
|
|
94
|
+
timeout,
|
|
95
|
+
maxBuffer: 1024 * 1024,
|
|
96
|
+
}),
|
|
97
|
+
commonDirPromise,
|
|
98
|
+
exec('git', ['-C', candidate, 'worktree', 'list', '--porcelain', '-z'], {
|
|
99
|
+
timeout,
|
|
100
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
101
|
+
}),
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
const topLevel = normalizePathValue(topLevelOutput);
|
|
105
|
+
const commonDir = normalizePathValue(commonDirOutput);
|
|
106
|
+
const worktrees = parseGitWorktreeList(listOutput);
|
|
107
|
+
const worktree = matchingWorktree(worktrees, topLevel || candidate);
|
|
108
|
+
if (!topLevel || !commonDir || !worktree) return null;
|
|
109
|
+
const mainWorktree = worktrees.find(record => !record.bare) || null;
|
|
110
|
+
const linked = Boolean(mainWorktree && mainWorktree.path !== worktree.path);
|
|
111
|
+
const mainWorkspace = mainWorktree ? mainWorktree.path : worktree.path;
|
|
112
|
+
const worktreeItems = worktrees.map(record => ({
|
|
113
|
+
workspace: record.path,
|
|
114
|
+
head: record.head || '',
|
|
115
|
+
branch: record.branch || '',
|
|
116
|
+
bare: record.bare === true,
|
|
117
|
+
detached: record.detached === true,
|
|
118
|
+
locked: record.locked === true,
|
|
119
|
+
lockReason: record.lockReason || '',
|
|
120
|
+
prunable: record.prunable === true,
|
|
121
|
+
pruneReason: record.pruneReason || '',
|
|
122
|
+
current: record.path === worktree.path,
|
|
123
|
+
main: record.path === mainWorkspace,
|
|
124
|
+
}));
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
workspace: worktree.path,
|
|
128
|
+
commonDir,
|
|
129
|
+
mainWorkspace,
|
|
130
|
+
linked,
|
|
131
|
+
branch: worktree.branch || '',
|
|
132
|
+
head: worktree.head || '',
|
|
133
|
+
detached: worktree.detached === true,
|
|
134
|
+
locked: worktree.locked === true,
|
|
135
|
+
lockReason: worktree.lockReason || '',
|
|
136
|
+
prunable: worktree.prunable === true,
|
|
137
|
+
pruneReason: worktree.pruneReason || '',
|
|
138
|
+
worktrees: worktreeItems,
|
|
139
|
+
};
|
|
140
|
+
} catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function inspectGitWorktree(workspace, options = {}) {
|
|
146
|
+
if (options.execFileAsync) return inspectGitWorktreeUncached(workspace, options);
|
|
147
|
+
const candidate = normalizePathValue(workspace);
|
|
148
|
+
if (!candidate) return null;
|
|
149
|
+
const now = Date.now();
|
|
150
|
+
const maxAgeMs = Number.isFinite(options.cacheMs)
|
|
151
|
+
? Math.max(0, options.cacheMs)
|
|
152
|
+
: DEFAULT_CACHE_MS;
|
|
153
|
+
const cached = cache.get(candidate);
|
|
154
|
+
if (cached && now - cached.createdAt <= maxAgeMs) return cached.promise;
|
|
155
|
+
|
|
156
|
+
const promise = inspectGitWorktreeUncached(candidate, options);
|
|
157
|
+
cache.set(candidate, { createdAt: now, promise });
|
|
158
|
+
promise.catch(() => {
|
|
159
|
+
if (cache.get(candidate)?.promise === promise) cache.delete(candidate);
|
|
160
|
+
});
|
|
161
|
+
return promise;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function isLinkedWorktreeOf(workspace, candidate, options = {}) {
|
|
165
|
+
const [sourceInfo, candidateInfo] = await Promise.all([
|
|
166
|
+
inspectGitWorktree(workspace, options),
|
|
167
|
+
inspectGitWorktree(candidate, options),
|
|
168
|
+
]);
|
|
169
|
+
return Boolean(
|
|
170
|
+
sourceInfo
|
|
171
|
+
&& candidateInfo
|
|
172
|
+
&& sourceInfo.commonDir === candidateInfo.commonDir
|
|
173
|
+
&& candidateInfo.linked
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
module.exports = {
|
|
178
|
+
inspectGitWorktree,
|
|
179
|
+
isLinkedWorktreeOf,
|
|
180
|
+
parseGitWorktreeList,
|
|
181
|
+
};
|