farming-code 2.2.20 → 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.
Files changed (49) hide show
  1. package/README.md +5 -5
  2. package/README.zh_cn.md +5 -5
  3. package/THIRD_PARTY_NOTICES.md +5 -8
  4. package/backend/acp/client-services.js +37 -5
  5. package/backend/acp/patch-decisions.js +19 -3
  6. package/backend/acp-runtime.js +1201 -198
  7. package/backend/acp-transcript.js +66 -0
  8. package/backend/agent-lifecycle-journal.js +133 -0
  9. package/backend/agent-manager.js +2715 -354
  10. package/backend/agent-order.js +8 -4
  11. package/backend/agent-runtime-binding.js +1 -0
  12. package/backend/bundle-update-helper.js +5 -1
  13. package/backend/config-manager.js +16 -0
  14. package/backend/control-api.js +146 -20
  15. package/backend/farming-app-cli.js +335 -10
  16. package/backend/farming-session-store.js +283 -36
  17. package/backend/json-cli-runtime.js +120 -6
  18. package/backend/npm-update-helper.js +97 -18
  19. package/backend/provider-adapters.js +18 -6
  20. package/backend/review-session-service.js +31 -17
  21. package/backend/server.js +332 -102
  22. package/backend/storage-layout.js +10 -0
  23. package/backend/theme-manager.js +23 -5
  24. package/backend/update-service.js +239 -27
  25. package/backend/workspace-file-router.js +9 -3
  26. package/backend/workspace-file-service.js +292 -62
  27. package/dist/acp/codex-acp-1.1.4.js +32 -1
  28. package/dist/assets/App-CmZdNavW.js +210 -0
  29. package/dist/assets/{FileEditorMarkdownPreview-R0jwwLME.js → FileEditorMarkdownPreview-KbnE8VC8.js} +1 -1
  30. package/dist/assets/FileEditorPane-5_HUejjd.js +2 -0
  31. package/dist/assets/IconGlyphs-C2WFcoJS.js +1 -0
  32. package/dist/assets/ProjectFilesSection-C1RAJWOE.js +12 -0
  33. package/dist/assets/{ReviewPage-DBKNOWsJ.js → ReviewPage-L1vh1Ggs.js} +3 -3
  34. package/dist/assets/code-dark-BKtvf0Xc.css +1 -0
  35. package/dist/assets/file-icons-CHWt5nSy.js +1 -0
  36. package/dist/assets/{index-DuMjesL2.js → index-RvUiMpGP.js} +2 -2
  37. package/dist/assets/main-XMXgyJJt.css +1 -0
  38. package/dist/assets/workspace-view-state-BqB4HM7r.js +7 -0
  39. package/dist/index.html +1 -1
  40. package/frontend/skins/crt/app.js +199 -81
  41. package/package.json +9 -3
  42. package/dist/assets/App-BupehIbc.js +0 -210
  43. package/dist/assets/FileEditorPane-CJs6kfZO.js +0 -2
  44. package/dist/assets/IconGlyphs-CZzDr5o2.js +0 -1
  45. package/dist/assets/ProjectFilesSection-DWCV07lA.js +0 -12
  46. package/dist/assets/code-dark-BAt0Q_Op.css +0 -1
  47. package/dist/assets/file-icons-DpbrD5ZP.js +0 -1
  48. package/dist/assets/main-y2aK_56c.css +0 -1
  49. package/dist/assets/workspace-view-state-BpPINu84.js +0 -7
@@ -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
- fs.writeFileSync(this.themeSettingsFile, JSON.stringify(this.userThemeSettings, null, 2));
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
- this.userThemeSettings[themeId] = {
149
+ const nextUserThemeSettings = {
136
150
  ...this.userThemeSettings[themeId],
137
151
  ...normalizedSettings
138
152
  };
139
-
140
- this.saveUserThemeSettings();
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
- await pipeline(response, fs.createWriteStream(outputPath));
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 child = this.spawn(helperInvocation.command, helperInvocation.args, {
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: 'installing',
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
 
@@ -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);