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
|
@@ -25,6 +25,10 @@ function acpCheckpointsDir(configDir) {
|
|
|
25
25
|
return path.join(sessionsDir(configDir), 'acp-checkpoints');
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
function agentStateFile(configDir, agentRecordId) {
|
|
29
|
+
return path.join(sessionsDir(configDir), `${agentRecordId}.state.json`);
|
|
30
|
+
}
|
|
31
|
+
|
|
28
32
|
function historyDir(configDir) {
|
|
29
33
|
return path.join(configDir, 'history');
|
|
30
34
|
}
|
|
@@ -89,6 +93,10 @@ function updateLogFile(configDir) {
|
|
|
89
93
|
return path.join(configDir, 'farming-update.log');
|
|
90
94
|
}
|
|
91
95
|
|
|
96
|
+
function updateStagingDir(configDir) {
|
|
97
|
+
return path.join(configDir, 'updates');
|
|
98
|
+
}
|
|
99
|
+
|
|
92
100
|
function farmingNetInstancesFile(configDir) {
|
|
93
101
|
return path.join(configDir, 'instances.json');
|
|
94
102
|
}
|
|
@@ -115,6 +123,7 @@ function farmingNetTrustFile(configDir) {
|
|
|
115
123
|
|
|
116
124
|
module.exports = {
|
|
117
125
|
acpCheckpointsDir,
|
|
126
|
+
agentStateFile,
|
|
118
127
|
farmingNetInstancesFile,
|
|
119
128
|
farmingNetServerLogFile,
|
|
120
129
|
farmingNetServerStateFile,
|
|
@@ -140,6 +149,7 @@ module.exports = {
|
|
|
140
149
|
settingsFile,
|
|
141
150
|
themeSettingsFile,
|
|
142
151
|
updateLogFile,
|
|
152
|
+
updateStagingDir,
|
|
143
153
|
updateStateFile,
|
|
144
154
|
usageHistoryCacheFile,
|
|
145
155
|
};
|
|
@@ -23,6 +23,7 @@ class TerminalScreenWorkerPool {
|
|
|
23
23
|
this.waiters = [];
|
|
24
24
|
this.readyWaiters = [];
|
|
25
25
|
this.pendingStarts = 0;
|
|
26
|
+
this.startTasks = new Set();
|
|
26
27
|
this.consecutiveStartFailures = 0;
|
|
27
28
|
this.retryDelayMs = Number.isFinite(options.retryDelayMs)
|
|
28
29
|
? Math.max(0, Number(options.retryDelayMs))
|
|
@@ -91,13 +92,12 @@ class TerminalScreenWorkerPool {
|
|
|
91
92
|
startWorker() {
|
|
92
93
|
this.pendingStarts += 1;
|
|
93
94
|
let failed = false;
|
|
94
|
-
this.createReadyWorker()
|
|
95
|
+
const startTask = this.createReadyWorker()
|
|
95
96
|
.then((worker) => {
|
|
96
97
|
this.pendingStarts -= 1;
|
|
97
98
|
this.consecutiveStartFailures = 0;
|
|
98
99
|
if (this.disposed) {
|
|
99
|
-
worker.dispose().catch(() => {});
|
|
100
|
-
return;
|
|
100
|
+
return worker.dispose().catch(() => {});
|
|
101
101
|
}
|
|
102
102
|
this.deliverWorker(worker);
|
|
103
103
|
})
|
|
@@ -122,6 +122,11 @@ class TerminalScreenWorkerPool {
|
|
|
122
122
|
this.ensureCapacity();
|
|
123
123
|
}
|
|
124
124
|
});
|
|
125
|
+
this.startTasks.add(startTask);
|
|
126
|
+
startTask.then(
|
|
127
|
+
() => this.startTasks.delete(startTask),
|
|
128
|
+
() => this.startTasks.delete(startTask),
|
|
129
|
+
);
|
|
125
130
|
}
|
|
126
131
|
|
|
127
132
|
scheduleCapacityRetry() {
|
|
@@ -197,8 +202,8 @@ class TerminalScreenWorkerPool {
|
|
|
197
202
|
}
|
|
198
203
|
|
|
199
204
|
async dispose() {
|
|
200
|
-
if (this.
|
|
201
|
-
return;
|
|
205
|
+
if (this.disposePromise) {
|
|
206
|
+
return this.disposePromise;
|
|
202
207
|
}
|
|
203
208
|
|
|
204
209
|
this.disposed = true;
|
|
@@ -210,7 +215,11 @@ class TerminalScreenWorkerPool {
|
|
|
210
215
|
this.waiters.splice(0).forEach(({ reject }) => reject(error));
|
|
211
216
|
this.readyWaiters.splice(0).forEach(({ reject }) => reject(error));
|
|
212
217
|
const workers = this.idle.splice(0);
|
|
213
|
-
|
|
218
|
+
this.disposePromise = Promise.allSettled([
|
|
219
|
+
...workers.map(worker => worker.dispose()),
|
|
220
|
+
...this.startTasks,
|
|
221
|
+
]).then(() => undefined);
|
|
222
|
+
return this.disposePromise;
|
|
214
223
|
}
|
|
215
224
|
}
|
|
216
225
|
|
package/backend/theme-manager.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const crypto = require('crypto');
|
|
3
4
|
const storageLayout = require('./storage-layout');
|
|
4
5
|
|
|
5
6
|
class ThemeManager {
|
|
@@ -79,14 +80,27 @@ class ThemeManager {
|
|
|
79
80
|
)));
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
saveUserThemeSettings() {
|
|
83
|
+
saveUserThemeSettings(settings = this.userThemeSettings) {
|
|
84
|
+
let temporaryFile = '';
|
|
83
85
|
try {
|
|
84
86
|
if (!fs.existsSync(this.farmingDir)) {
|
|
85
87
|
fs.mkdirSync(this.farmingDir, { recursive: true });
|
|
86
88
|
}
|
|
87
|
-
|
|
89
|
+
temporaryFile = `${this.themeSettingsFile}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
90
|
+
fs.writeFileSync(temporaryFile, JSON.stringify(settings, null, 2), { flag: 'wx', mode: 0o600 });
|
|
91
|
+
fs.renameSync(temporaryFile, this.themeSettingsFile);
|
|
92
|
+
return true;
|
|
88
93
|
} catch (error) {
|
|
89
94
|
console.error('Failed to save user theme settings:', error.message);
|
|
95
|
+
return false;
|
|
96
|
+
} finally {
|
|
97
|
+
if (temporaryFile) {
|
|
98
|
+
try {
|
|
99
|
+
fs.unlinkSync(temporaryFile);
|
|
100
|
+
} catch {
|
|
101
|
+
// A successful rename already removed the temporary path.
|
|
102
|
+
}
|
|
103
|
+
}
|
|
90
104
|
}
|
|
91
105
|
}
|
|
92
106
|
|
|
@@ -132,12 +146,16 @@ class ThemeManager {
|
|
|
132
146
|
}
|
|
133
147
|
|
|
134
148
|
const normalizedSettings = this.normalizeThemeSettings(settings);
|
|
135
|
-
|
|
149
|
+
const nextUserThemeSettings = {
|
|
136
150
|
...this.userThemeSettings[themeId],
|
|
137
151
|
...normalizedSettings
|
|
138
152
|
};
|
|
139
|
-
|
|
140
|
-
|
|
153
|
+
const nextSettings = {
|
|
154
|
+
...this.userThemeSettings,
|
|
155
|
+
[themeId]: nextUserThemeSettings,
|
|
156
|
+
};
|
|
157
|
+
if (!this.saveUserThemeSettings(nextSettings)) return false;
|
|
158
|
+
this.userThemeSettings = nextSettings;
|
|
141
159
|
return true;
|
|
142
160
|
}
|
|
143
161
|
}
|
|
@@ -5,6 +5,7 @@ const http = require('http');
|
|
|
5
5
|
const https = require('https');
|
|
6
6
|
const os = require('os');
|
|
7
7
|
const path = require('path');
|
|
8
|
+
const { Transform } = require('stream');
|
|
8
9
|
const { pipeline } = require('stream/promises');
|
|
9
10
|
const storageLayout = require('./storage-layout');
|
|
10
11
|
|
|
@@ -581,7 +582,26 @@ async function downloadFile(url, outputPath, options = {}) {
|
|
|
581
582
|
...options,
|
|
582
583
|
accept: 'application/octet-stream',
|
|
583
584
|
});
|
|
584
|
-
|
|
585
|
+
const responseLength = Number(response.headers['content-length']);
|
|
586
|
+
const expectedLength = Number(options.totalBytes);
|
|
587
|
+
const totalBytes = Number.isFinite(responseLength) && responseLength > 0
|
|
588
|
+
? responseLength
|
|
589
|
+
: (Number.isFinite(expectedLength) && expectedLength > 0 ? expectedLength : 0);
|
|
590
|
+
let receivedBytes = 0;
|
|
591
|
+
const trackProgress = new Transform({
|
|
592
|
+
transform(chunk, _encoding, callback) {
|
|
593
|
+
receivedBytes += chunk.length;
|
|
594
|
+
try {
|
|
595
|
+
if (typeof options.onProgress === 'function') {
|
|
596
|
+
options.onProgress({ receivedBytes, totalBytes });
|
|
597
|
+
}
|
|
598
|
+
callback(null, chunk);
|
|
599
|
+
} catch (error) {
|
|
600
|
+
callback(error);
|
|
601
|
+
}
|
|
602
|
+
},
|
|
603
|
+
});
|
|
604
|
+
await pipeline(response, trackProgress, fs.createWriteStream(outputPath));
|
|
585
605
|
}
|
|
586
606
|
|
|
587
607
|
function sha256File(filePath) {
|
|
@@ -685,6 +705,11 @@ function normalizePathForCompare(filePath) {
|
|
|
685
705
|
}
|
|
686
706
|
}
|
|
687
707
|
|
|
708
|
+
function pathIsInside(parentPath, childPath) {
|
|
709
|
+
const relative = path.relative(path.resolve(parentPath), path.resolve(childPath));
|
|
710
|
+
return relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative);
|
|
711
|
+
}
|
|
712
|
+
|
|
688
713
|
function npmPackageRoot(npmRoot, packageName) {
|
|
689
714
|
return path.join(npmRoot, ...String(packageName || '').split('/').filter(Boolean));
|
|
690
715
|
}
|
|
@@ -767,8 +792,10 @@ class FarmingUpdateService {
|
|
|
767
792
|
this.latestCache = null;
|
|
768
793
|
this.npmCache = null;
|
|
769
794
|
this.installState = { phase: 'idle' };
|
|
795
|
+
this.installStartPromise = null;
|
|
770
796
|
this.updateStateFile = options.updateStateFile || storageLayout.updateStateFile(this.configDir);
|
|
771
797
|
this.updateLogFile = options.updateLogFile || storageLayout.updateLogFile(this.configDir);
|
|
798
|
+
this.updateStagingDir = options.updateStagingDir || storageLayout.updateStagingDir(this.configDir);
|
|
772
799
|
}
|
|
773
800
|
|
|
774
801
|
updateUrl() {
|
|
@@ -1089,10 +1116,22 @@ class FarmingUpdateService {
|
|
|
1089
1116
|
}
|
|
1090
1117
|
|
|
1091
1118
|
async startInstall(options = {}) {
|
|
1119
|
+
if (this.installStartPromise) return this.installStartPromise;
|
|
1120
|
+
const startPromise = this.startInstallUnreserved(options);
|
|
1121
|
+
this.installStartPromise = startPromise;
|
|
1122
|
+
try {
|
|
1123
|
+
return await startPromise;
|
|
1124
|
+
} finally {
|
|
1125
|
+
if (this.installStartPromise === startPromise) this.installStartPromise = null;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
async startInstallUnreserved(options = {}) {
|
|
1092
1130
|
const currentState = this.currentInstallState();
|
|
1093
1131
|
if (['downloading', 'extracting', 'installing', 'restarting', 'rolling-back'].includes(currentState.phase)) {
|
|
1094
1132
|
return currentState;
|
|
1095
1133
|
}
|
|
1134
|
+
if (currentState.phase === 'ready-to-restart') return currentState;
|
|
1096
1135
|
|
|
1097
1136
|
if (this.installMethod === 'npm') return this.startNpmInstall(options);
|
|
1098
1137
|
if (!installMethodAllowsBundleUpdate(this.installMethod)) {
|
|
@@ -1127,6 +1166,8 @@ class FarmingUpdateService {
|
|
|
1127
1166
|
version: status.selected.version,
|
|
1128
1167
|
previousVersion: status.current.releaseVersion || status.current.packageVersion,
|
|
1129
1168
|
assetName: status.selected.assetName,
|
|
1169
|
+
receivedBytes: 0,
|
|
1170
|
+
totalBytes: Number(status.selected.assetSize) || 0,
|
|
1130
1171
|
startedAt: new Date(this.now()).toISOString(),
|
|
1131
1172
|
logPath: path.join(this.configDir, 'farming-update.log'),
|
|
1132
1173
|
});
|
|
@@ -1154,18 +1195,27 @@ class FarmingUpdateService {
|
|
|
1154
1195
|
}
|
|
1155
1196
|
|
|
1156
1197
|
const startedAt = new Date(this.now()).toISOString();
|
|
1198
|
+
fs.mkdirSync(this.updateStagingDir, { recursive: true });
|
|
1199
|
+
const stagingPrefix = fs.mkdtempSync(path.join(this.updateStagingDir, `npm-${status.selected.version}.`));
|
|
1200
|
+
const stagingPackageRoot = npmPackageRoot(
|
|
1201
|
+
path.join(stagingPrefix, 'lib', 'node_modules'),
|
|
1202
|
+
this.npmPackageName,
|
|
1203
|
+
);
|
|
1157
1204
|
const state = this.persistInstallState({
|
|
1158
1205
|
method: 'npm',
|
|
1159
1206
|
phase: 'installing',
|
|
1160
1207
|
version: status.selected.version,
|
|
1161
1208
|
previousVersion: status.current.releaseVersion || status.current.packageVersion,
|
|
1162
1209
|
packageName: this.npmPackageName,
|
|
1210
|
+
stagingPrefix,
|
|
1211
|
+
stagingPackageRoot,
|
|
1163
1212
|
startedAt,
|
|
1164
1213
|
logPath: this.updateLogFile,
|
|
1165
1214
|
});
|
|
1166
1215
|
const helperPath = path.join(__dirname, 'npm-update-helper.js');
|
|
1167
1216
|
const nodePath = process.env.FARMING_NODE_BIN || process.execPath;
|
|
1168
1217
|
const payload = {
|
|
1218
|
+
action: 'prepare',
|
|
1169
1219
|
packageName: this.npmPackageName,
|
|
1170
1220
|
targetVersion: status.selected.version,
|
|
1171
1221
|
previousVersion: status.current.releaseVersion || status.current.packageVersion,
|
|
@@ -1177,6 +1227,8 @@ class FarmingUpdateService {
|
|
|
1177
1227
|
nodePath,
|
|
1178
1228
|
npmCommand: process.env.FARMING_NPM_COMMAND || 'npm',
|
|
1179
1229
|
npmPrefix: status.target.npmPrefix,
|
|
1230
|
+
stagingPrefix,
|
|
1231
|
+
stagingPackageRoot,
|
|
1180
1232
|
npmFallbackRegistryUrl: this.npmRegistryUrl,
|
|
1181
1233
|
serverPid: process.pid,
|
|
1182
1234
|
configDir: this.configDir,
|
|
@@ -1186,7 +1238,7 @@ class FarmingUpdateService {
|
|
|
1186
1238
|
disableAuth: /^(1|true|yes|on)$/i.test(String(process.env.FARMING_DISABLE_AUTH || '')),
|
|
1187
1239
|
};
|
|
1188
1240
|
const helperInvocation = nodeScriptInvocation(nodePath, helperPath);
|
|
1189
|
-
const
|
|
1241
|
+
const spawnOptions = {
|
|
1190
1242
|
cwd: this.configDir,
|
|
1191
1243
|
detached: true,
|
|
1192
1244
|
stdio: 'ignore',
|
|
@@ -1194,7 +1246,169 @@ class FarmingUpdateService {
|
|
|
1194
1246
|
...process.env,
|
|
1195
1247
|
FARMING_NPM_UPDATE_PAYLOAD: JSON.stringify(payload),
|
|
1196
1248
|
},
|
|
1249
|
+
};
|
|
1250
|
+
let child;
|
|
1251
|
+
try {
|
|
1252
|
+
child = this.spawn(helperInvocation.command, helperInvocation.args, spawnOptions);
|
|
1253
|
+
} catch (error) {
|
|
1254
|
+
fs.rmSync(stagingPrefix, { recursive: true, force: true });
|
|
1255
|
+
this.persistInstallState({
|
|
1256
|
+
...state,
|
|
1257
|
+
phase: 'failed',
|
|
1258
|
+
error: error.message || String(error),
|
|
1259
|
+
completedAt: new Date(this.now()).toISOString(),
|
|
1260
|
+
});
|
|
1261
|
+
throw error;
|
|
1262
|
+
}
|
|
1263
|
+
if (child && typeof child.once === 'function') {
|
|
1264
|
+
child.once('error', (error) => {
|
|
1265
|
+
fs.rmSync(stagingPrefix, { recursive: true, force: true });
|
|
1266
|
+
this.persistInstallState({
|
|
1267
|
+
...state,
|
|
1268
|
+
phase: 'failed',
|
|
1269
|
+
error: error.message || String(error),
|
|
1270
|
+
completedAt: new Date(this.now()).toISOString(),
|
|
1271
|
+
});
|
|
1272
|
+
});
|
|
1273
|
+
}
|
|
1274
|
+
if (child && typeof child.unref === 'function') child.unref();
|
|
1275
|
+
return state;
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
async applyPreparedUpdate() {
|
|
1279
|
+
if (this.installStartPromise) return this.installStartPromise;
|
|
1280
|
+
const applyPromise = this.applyPreparedUpdateUnreserved();
|
|
1281
|
+
this.installStartPromise = applyPromise;
|
|
1282
|
+
try {
|
|
1283
|
+
return await applyPromise;
|
|
1284
|
+
} finally {
|
|
1285
|
+
if (this.installStartPromise === applyPromise) this.installStartPromise = null;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
async applyPreparedUpdateUnreserved() {
|
|
1290
|
+
const prepared = this.currentInstallState();
|
|
1291
|
+
if (prepared.phase !== 'ready-to-restart') {
|
|
1292
|
+
throw new Error('No prepared Farming update is ready to restart');
|
|
1293
|
+
}
|
|
1294
|
+
const current = this.currentVersion();
|
|
1295
|
+
const currentVersion = normalizeVersion(current.releaseVersion || current.packageVersion);
|
|
1296
|
+
if (normalizeVersion(prepared.previousVersion) !== currentVersion) {
|
|
1297
|
+
const error = 'Prepared update no longer matches the running Farming version';
|
|
1298
|
+
this.persistInstallState({ ...prepared, phase: 'failed', error, completedAt: new Date(this.now()).toISOString() });
|
|
1299
|
+
throw new Error(error);
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
const nodePath = process.env.FARMING_NODE_BIN || process.execPath;
|
|
1303
|
+
let helperPath;
|
|
1304
|
+
let payload;
|
|
1305
|
+
let env;
|
|
1306
|
+
if (this.installMethod === 'npm') {
|
|
1307
|
+
const target = await this.npmUpdateTarget();
|
|
1308
|
+
if (!target.proven) throw new Error(target.error);
|
|
1309
|
+
if (!path.isAbsolute(String(prepared.stagingPrefix || '')) || !path.isAbsolute(String(prepared.stagingPackageRoot || ''))) {
|
|
1310
|
+
throw new Error('Prepared npm update is missing its staging identity');
|
|
1311
|
+
}
|
|
1312
|
+
const stagingPrefix = normalizePathForCompare(prepared.stagingPrefix);
|
|
1313
|
+
if (!pathIsInside(normalizePathForCompare(this.updateStagingDir), stagingPrefix)) {
|
|
1314
|
+
throw new Error('Prepared npm update is outside the Farming staging directory');
|
|
1315
|
+
}
|
|
1316
|
+
if (path.resolve(prepared.stagingPackageRoot) !== path.resolve(npmPackageRoot(
|
|
1317
|
+
path.join(prepared.stagingPrefix, 'lib', 'node_modules'),
|
|
1318
|
+
this.npmPackageName,
|
|
1319
|
+
))) {
|
|
1320
|
+
throw new Error('Prepared npm update has an invalid package root');
|
|
1321
|
+
}
|
|
1322
|
+
const stagingPackageRoot = normalizePathForCompare(prepared.stagingPackageRoot);
|
|
1323
|
+
const stagedMetadata = readJsonFile(path.join(stagingPackageRoot, 'package.json')) || {};
|
|
1324
|
+
if (normalizeVersion(stagedMetadata.version) !== normalizeVersion(prepared.version)) {
|
|
1325
|
+
throw new Error('Prepared npm update is no longer available; prepare it again');
|
|
1326
|
+
}
|
|
1327
|
+
helperPath = path.join(__dirname, 'npm-update-helper.js');
|
|
1328
|
+
payload = {
|
|
1329
|
+
action: 'apply',
|
|
1330
|
+
packageName: this.npmPackageName,
|
|
1331
|
+
targetVersion: prepared.version,
|
|
1332
|
+
previousVersion: prepared.previousVersion,
|
|
1333
|
+
startedAt: prepared.startedAt,
|
|
1334
|
+
preparedAt: prepared.preparedAt,
|
|
1335
|
+
stateFile: this.updateStateFile,
|
|
1336
|
+
logPath: prepared.logPath || this.updateLogFile,
|
|
1337
|
+
cliPath: path.join(this.rootDir, 'bin', 'farming'),
|
|
1338
|
+
packageRoot: target.packageRoot,
|
|
1339
|
+
nodePath,
|
|
1340
|
+
npmCommand: process.env.FARMING_NPM_COMMAND || 'npm',
|
|
1341
|
+
npmPrefix: target.npmPrefix,
|
|
1342
|
+
npmFallbackRegistryUrl: this.npmRegistryUrl,
|
|
1343
|
+
stagingPrefix,
|
|
1344
|
+
stagingPackageRoot,
|
|
1345
|
+
serverPid: process.pid,
|
|
1346
|
+
configDir: this.configDir,
|
|
1347
|
+
port: process.env.FARMING_PORT || process.env.PORT || '6694',
|
|
1348
|
+
basePath: process.env.FARMING_BASE_PATH || '/farming',
|
|
1349
|
+
serverHome: process.env.FARMING_SERVER_HOME || '',
|
|
1350
|
+
disableAuth: /^(1|true|yes|on)$/i.test(String(process.env.FARMING_DISABLE_AUTH || '')),
|
|
1351
|
+
};
|
|
1352
|
+
env = { ...process.env, FARMING_NPM_UPDATE_PAYLOAD: JSON.stringify(payload) };
|
|
1353
|
+
} else {
|
|
1354
|
+
if (!installMethodAllowsBundleUpdate(this.installMethod)) {
|
|
1355
|
+
throw new Error(installMethodBlockedReason(this.installMethod));
|
|
1356
|
+
}
|
|
1357
|
+
if (!path.isAbsolute(String(prepared.releaseDir || '')) || !path.isAbsolute(String(prepared.installer || '')) || !fs.existsSync(prepared.installer)) {
|
|
1358
|
+
throw new Error('Prepared bundle update is no longer available; prepare it again');
|
|
1359
|
+
}
|
|
1360
|
+
const releaseDir = normalizePathForCompare(prepared.releaseDir);
|
|
1361
|
+
const installer = normalizePathForCompare(prepared.installer);
|
|
1362
|
+
if (!pathIsInside(normalizePathForCompare(os.tmpdir()), releaseDir)
|
|
1363
|
+
|| !path.basename(path.dirname(releaseDir)).startsWith('farming-update.')
|
|
1364
|
+
|| installer !== normalizePathForCompare(path.join(releaseDir, 'scripts', 'install-release.sh'))) {
|
|
1365
|
+
throw new Error('Prepared bundle update has an invalid staging identity');
|
|
1366
|
+
}
|
|
1367
|
+
const releaseMetadata = readJsonFile(path.join(releaseDir, 'RELEASE.json')) || {};
|
|
1368
|
+
const preparedVersion = normalizeVersion(releaseMetadata.releaseVersion || releaseMetadata.packageVersion);
|
|
1369
|
+
if (preparedVersion && preparedVersion !== normalizeVersion(prepared.version)) {
|
|
1370
|
+
throw new Error('Prepared bundle version does not match the selected update');
|
|
1371
|
+
}
|
|
1372
|
+
if (!assetMatchesRuntime(releaseMetadata, this.runtime)) {
|
|
1373
|
+
throw new Error(`Prepared bundle is not compatible with ${this.runtime.platform}-${this.runtime.arch}`);
|
|
1374
|
+
}
|
|
1375
|
+
helperPath = path.join(__dirname, 'bundle-update-helper.js');
|
|
1376
|
+
payload = {
|
|
1377
|
+
method: this.installMethod,
|
|
1378
|
+
targetMethod: prepared.targetMethod,
|
|
1379
|
+
version: prepared.version,
|
|
1380
|
+
previousVersion: prepared.previousVersion,
|
|
1381
|
+
startedAt: prepared.startedAt,
|
|
1382
|
+
preparedAt: prepared.preparedAt,
|
|
1383
|
+
stateFile: this.updateStateFile,
|
|
1384
|
+
logPath: prepared.logPath || this.updateLogFile,
|
|
1385
|
+
releaseDir,
|
|
1386
|
+
installer,
|
|
1387
|
+
};
|
|
1388
|
+
env = { ...this.installEnvironment(), FARMING_BUNDLE_UPDATE_PAYLOAD: JSON.stringify(payload) };
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
const state = this.persistInstallState({ ...prepared, phase: 'restarting', error: '' });
|
|
1392
|
+
const helperInvocation = nodeScriptInvocation(nodePath, helperPath);
|
|
1393
|
+
const spawnOptions = {
|
|
1394
|
+
cwd: this.configDir,
|
|
1395
|
+
detached: true,
|
|
1396
|
+
stdio: 'ignore',
|
|
1397
|
+
env,
|
|
1398
|
+
};
|
|
1399
|
+
const restorePreparedState = (error) => this.persistInstallState({
|
|
1400
|
+
...prepared,
|
|
1401
|
+
phase: 'ready-to-restart',
|
|
1402
|
+
error: error.message || String(error),
|
|
1197
1403
|
});
|
|
1404
|
+
let child;
|
|
1405
|
+
try {
|
|
1406
|
+
child = this.spawn(helperInvocation.command, helperInvocation.args, spawnOptions);
|
|
1407
|
+
} catch (error) {
|
|
1408
|
+
restorePreparedState(error);
|
|
1409
|
+
throw error;
|
|
1410
|
+
}
|
|
1411
|
+
if (child && typeof child.once === 'function') child.once('error', restorePreparedState);
|
|
1198
1412
|
if (child && typeof child.unref === 'function') child.unref();
|
|
1199
1413
|
return state;
|
|
1200
1414
|
}
|
|
@@ -1207,8 +1421,28 @@ class FarmingUpdateService {
|
|
|
1207
1421
|
fs.mkdirSync(this.configDir, { recursive: true });
|
|
1208
1422
|
const tempRoot = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'farming-update.'));
|
|
1209
1423
|
const archivePath = path.join(tempRoot, asset.name || 'farming-update.tar.gz');
|
|
1424
|
+
let lastProgressPercent = -1;
|
|
1425
|
+
let lastProgressBytes = 0;
|
|
1210
1426
|
await this.downloadFile(asset.browser_download_url, archivePath, {
|
|
1211
1427
|
authToken: this.authToken,
|
|
1428
|
+
totalBytes: Number(asset.size) || Number(this.installState.totalBytes) || 0,
|
|
1429
|
+
onProgress: ({ receivedBytes, totalBytes }) => {
|
|
1430
|
+
const safeReceivedBytes = Math.max(0, Number(receivedBytes) || 0);
|
|
1431
|
+
const safeTotalBytes = Math.max(0, Number(totalBytes) || 0);
|
|
1432
|
+
if (safeTotalBytes > 0) {
|
|
1433
|
+
const percent = Math.min(100, Math.floor((safeReceivedBytes / safeTotalBytes) * 100));
|
|
1434
|
+
if (percent === lastProgressPercent && safeReceivedBytes < safeTotalBytes) return;
|
|
1435
|
+
lastProgressPercent = percent;
|
|
1436
|
+
} else if (safeReceivedBytes - lastProgressBytes < 1024 * 1024) {
|
|
1437
|
+
return;
|
|
1438
|
+
}
|
|
1439
|
+
lastProgressBytes = safeReceivedBytes;
|
|
1440
|
+
this.persistInstallState({
|
|
1441
|
+
...this.installState,
|
|
1442
|
+
receivedBytes: safeReceivedBytes,
|
|
1443
|
+
totalBytes: safeTotalBytes,
|
|
1444
|
+
});
|
|
1445
|
+
},
|
|
1212
1446
|
});
|
|
1213
1447
|
let expectedSha256 = normalizeSha256(asset.sha256);
|
|
1214
1448
|
const checksumUrl = asset.checksum_url || asset.checksumUrl || '';
|
|
@@ -1249,37 +1483,15 @@ class FarmingUpdateService {
|
|
|
1249
1483
|
}
|
|
1250
1484
|
const logPath = this.installState.logPath || path.join(this.configDir, 'farming-update.log');
|
|
1251
1485
|
const targetMethod = String(releaseMetadata.updateMethod || releaseMetadata.type || this.installMethod);
|
|
1252
|
-
const helperPath = path.join(__dirname, 'bundle-update-helper.js');
|
|
1253
|
-
const nodePath = process.env.FARMING_NODE_BIN || process.execPath;
|
|
1254
|
-
const helperInvocation = nodeScriptInvocation(nodePath, helperPath);
|
|
1255
|
-
const payload = {
|
|
1256
|
-
method: this.installMethod,
|
|
1257
|
-
targetMethod,
|
|
1258
|
-
version: this.installState.version,
|
|
1259
|
-
previousVersion: this.installState.previousVersion,
|
|
1260
|
-
startedAt: this.installState.startedAt,
|
|
1261
|
-
stateFile: this.updateStateFile,
|
|
1262
|
-
logPath,
|
|
1263
|
-
releaseDir,
|
|
1264
|
-
installer,
|
|
1265
|
-
};
|
|
1266
1486
|
this.persistInstallState({
|
|
1267
1487
|
...this.installState,
|
|
1268
|
-
phase: '
|
|
1488
|
+
phase: 'ready-to-restart',
|
|
1269
1489
|
targetMethod,
|
|
1270
1490
|
releaseDir,
|
|
1491
|
+
installer,
|
|
1271
1492
|
logPath,
|
|
1493
|
+
preparedAt: new Date(this.now()).toISOString(),
|
|
1272
1494
|
});
|
|
1273
|
-
const child = this.spawn(helperInvocation.command, helperInvocation.args, {
|
|
1274
|
-
cwd: this.configDir,
|
|
1275
|
-
detached: true,
|
|
1276
|
-
stdio: 'ignore',
|
|
1277
|
-
env: {
|
|
1278
|
-
...this.installEnvironment(),
|
|
1279
|
-
FARMING_BUNDLE_UPDATE_PAYLOAD: JSON.stringify(payload),
|
|
1280
|
-
},
|
|
1281
|
-
});
|
|
1282
|
-
if (child && typeof child.unref === 'function') child.unref();
|
|
1283
1495
|
}
|
|
1284
1496
|
}
|
|
1285
1497
|
|
package/backend/usage-monitor.js
CHANGED
|
@@ -10,6 +10,8 @@ const execFileAsync = promisify(execFile);
|
|
|
10
10
|
const USAGE_WINDOW_MS = 5 * 60 * 1000;
|
|
11
11
|
const USAGE_TIMELINE_WINDOW_MS = 24 * 60 * 60 * 1000;
|
|
12
12
|
const USAGE_TIMELINE_BUCKET_COUNT = 24;
|
|
13
|
+
const USAGE_LIVE_TIMELINE_WINDOW_MS = 60 * 60 * 1000;
|
|
14
|
+
const USAGE_LIVE_TIMELINE_BUCKET_COUNT = 60;
|
|
13
15
|
const USAGE_DAILY_DAYS = 52 * 7;
|
|
14
16
|
const USAGE_DAILY_CACHE_MS = 5 * 60 * 1000;
|
|
15
17
|
const USAGE_LIVE_DAY_CACHE_MS = 5 * 1000;
|
|
@@ -1020,6 +1022,12 @@ class UsageMonitor {
|
|
|
1020
1022
|
windowMs: historyWindowMs,
|
|
1021
1023
|
alignToBucket: true,
|
|
1022
1024
|
});
|
|
1025
|
+
const liveTimeline = buildUsageTimeline(history.providerEvents, {
|
|
1026
|
+
now,
|
|
1027
|
+
windowMs: USAGE_LIVE_TIMELINE_WINDOW_MS,
|
|
1028
|
+
bucketCount: USAGE_LIVE_TIMELINE_BUCKET_COUNT,
|
|
1029
|
+
alignToBucket: true,
|
|
1030
|
+
});
|
|
1023
1031
|
const openCodeCoverage = history.coverage.find(entry => entry.provider === 'opencode');
|
|
1024
1032
|
const qoderCoverage = history.coverage.find(entry => entry.provider === 'qoder');
|
|
1025
1033
|
|
|
@@ -1027,6 +1035,7 @@ class UsageMonitor {
|
|
|
1027
1035
|
sampledAt: now,
|
|
1028
1036
|
windowMs,
|
|
1029
1037
|
timeline,
|
|
1038
|
+
liveTimeline,
|
|
1030
1039
|
daily: history.daily,
|
|
1031
1040
|
providers: [
|
|
1032
1041
|
{
|
|
@@ -1099,6 +1108,8 @@ module.exports = {
|
|
|
1099
1108
|
USAGE_WINDOW_MS,
|
|
1100
1109
|
USAGE_TIMELINE_WINDOW_MS,
|
|
1101
1110
|
USAGE_TIMELINE_BUCKET_COUNT,
|
|
1111
|
+
USAGE_LIVE_TIMELINE_WINDOW_MS,
|
|
1112
|
+
USAGE_LIVE_TIMELINE_BUCKET_COUNT,
|
|
1102
1113
|
USAGE_DAILY_DAYS,
|
|
1103
1114
|
USAGE_DAILY_CACHE_MS,
|
|
1104
1115
|
USAGE_LIVE_DAY_CACHE_MS,
|
|
@@ -286,7 +286,9 @@ function createWorkspaceFileRouter(agentManager, fileService, options = {}) {
|
|
|
286
286
|
const rootRef = workspaceRef(body);
|
|
287
287
|
assertWritableWorkspaceAgent(rootRef);
|
|
288
288
|
const { root, rootId } = resolveRequestRoot(body);
|
|
289
|
-
const move = await fileService.moveEntry(root, body.sourcePath || '', body.targetDirectory || ''
|
|
289
|
+
const move = await fileService.moveEntry(root, body.sourcePath || '', body.targetDirectory || '', {
|
|
290
|
+
expectedVersion: body.expectedVersion,
|
|
291
|
+
});
|
|
290
292
|
res.json({ rootId, root, move });
|
|
291
293
|
} catch (error) {
|
|
292
294
|
sendWorkspaceFileError(res, error);
|
|
@@ -312,7 +314,9 @@ function createWorkspaceFileRouter(agentManager, fileService, options = {}) {
|
|
|
312
314
|
const rootRef = workspaceRef(body);
|
|
313
315
|
assertWritableWorkspaceAgent(rootRef);
|
|
314
316
|
const { root, rootId } = resolveRequestRoot(body);
|
|
315
|
-
const move = await fileService.renameEntry(root, body.path || '', body.name || ''
|
|
317
|
+
const move = await fileService.renameEntry(root, body.path || '', body.name || '', {
|
|
318
|
+
expectedVersion: body.expectedVersion,
|
|
319
|
+
});
|
|
316
320
|
res.json({ rootId, root, move });
|
|
317
321
|
} catch (error) {
|
|
318
322
|
sendWorkspaceFileError(res, error);
|
|
@@ -327,7 +331,9 @@ function createWorkspaceFileRouter(agentManager, fileService, options = {}) {
|
|
|
327
331
|
assertWritableWorkspaceAgent(rootRef);
|
|
328
332
|
const workspaceRoot = rootRegistry.resolve(rootRef);
|
|
329
333
|
const { canonicalPath: root, rootId } = workspaceRoot;
|
|
330
|
-
const deleted = await fileService.deleteEntry(root, targetPath
|
|
334
|
+
const deleted = await fileService.deleteEntry(root, targetPath, {
|
|
335
|
+
expectedVersion: body.expectedVersion || req.query.expectedVersion,
|
|
336
|
+
});
|
|
331
337
|
res.json({ rootId, root, deleted });
|
|
332
338
|
} catch (error) {
|
|
333
339
|
sendWorkspaceFileError(res, error);
|