farming-code 2.2.17 → 2.2.19

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.
@@ -79,6 +79,9 @@ const AGENT_DISCOVERY_CACHE_MAX_AGE_MS = 3_000;
79
79
  const UNCERTAIN_TERMINAL_STOP_TIMEOUT_MS = 5_000;
80
80
  const TERMINAL_STOP_STATE_READ_TIMEOUT_MS = 1_000;
81
81
  const TERMINAL_STOP_POLL_MS = 50;
82
+ const CODEX_TERMINAL_START_READY_TIMEOUT_MS = 30_000;
83
+ const CODEX_TERMINAL_START_READY_POLL_MS = 50;
84
+ const CODEX_TERMINAL_START_OUTPUT_LIMIT = 64 * 1024;
82
85
  const SHELL_PROMPT_ENV_KEYS = [
83
86
  'PS1',
84
87
  'PS2',
@@ -100,6 +103,15 @@ function withBoundedWait(promise, timeoutMs, label) {
100
103
  return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
101
104
  }
102
105
 
106
+ function canonicalProviderHomePath(value) {
107
+ const resolved = path.resolve(String(value || '').trim() || '.');
108
+ try {
109
+ return fs.realpathSync.native(resolved);
110
+ } catch {
111
+ return resolved;
112
+ }
113
+ }
114
+
103
115
  async function deletePrecreatedProviderSession(options = {}) {
104
116
  const provider = String(options.provider || '').trim().toLowerCase();
105
117
  const sessionId = String(options.sessionId || '').trim();
@@ -591,6 +603,8 @@ class AgentManager extends EventEmitter {
591
603
  this.resizeDrains = new Map();
592
604
  this.inputQueues = new Map();
593
605
  this.codexTerminalProfileQueues = new Map();
606
+ this.codexTerminalStartQueues = new Map();
607
+ this.codexTerminalStartOutput = new Map();
594
608
  this.agentWorktreeResolveGeneration = new Map();
595
609
  this.permissionRestartInFlight = new Map();
596
610
  this.runtimeRestartInFlight = new Map();
@@ -757,6 +771,13 @@ class AgentManager extends EventEmitter {
757
771
 
758
772
  this.reviveAgentRuntime(agent);
759
773
  agent.output = trimSessionOutput(agent.output + data);
774
+ if (this.codexTerminalStartOutput.has(sessionId)) {
775
+ const startupOutput = `${this.codexTerminalStartOutput.get(sessionId) || ''}${data}`;
776
+ this.codexTerminalStartOutput.set(
777
+ sessionId,
778
+ startupOutput.slice(-CODEX_TERMINAL_START_OUTPUT_LIMIT),
779
+ );
780
+ }
760
781
  if (agent.providerSessionProvider === 'codex' && agent.providerSessionTemporary === true) {
761
782
  void this.providerSessionService.resolveTemporaryCodex(sessionId);
762
783
  }
@@ -1393,6 +1414,16 @@ class AgentManager extends EventEmitter {
1393
1414
  projectOrder: finiteOrder(record.projectOrder),
1394
1415
  pinnedOrder: finiteOrder(record.pinnedOrder),
1395
1416
  customTitle: record.customTitle || '',
1417
+ pinned: record.pinned === true,
1418
+ attentionSeq: finiteNonNegativeInteger(record.attentionSeq),
1419
+ readAttentionSeq: finiteNonNegativeInteger(record.readAttentionSeq),
1420
+ attentionUpdatedAt: finiteNumberOrNull(record.attentionUpdatedAt),
1421
+ readAttentionAt: finiteNumberOrNull(record.readAttentionAt),
1422
+ attentionReason: record.attentionReason || '',
1423
+ attentionOutputEpoch: record.attentionOutputEpoch || '',
1424
+ attentionOutputSeq: finiteNumberOrNull(record.attentionOutputSeq),
1425
+ readOutputEpoch: record.readOutputEpoch || '',
1426
+ readOutputSeq: finiteNumberOrNull(record.readOutputSeq),
1396
1427
  persistentSessionId: record.id || '',
1397
1428
  runtimeAgentId: record.runtimeAgentId || '',
1398
1429
  reviveTerminalState: record.serializedState || null,
@@ -1440,7 +1471,10 @@ class AgentManager extends EventEmitter {
1440
1471
  replacement.attentionUpdatedAt = finiteNumberOrNull(record.attentionUpdatedAt);
1441
1472
  replacement.readAttentionAt = finiteNumberOrNull(record.readAttentionAt);
1442
1473
  replacement.attentionReason = record.attentionReason || '';
1474
+ replacement.attentionOutputEpoch = record.attentionOutputEpoch || '';
1443
1475
  replacement.attentionOutputSeq = finiteNumberOrNull(record.attentionOutputSeq);
1476
+ replacement.readOutputEpoch = record.readOutputEpoch || '';
1477
+ replacement.readOutputSeq = finiteNumberOrNull(record.readOutputSeq);
1444
1478
  replacement.unread = agentAttentionUnread(replacement);
1445
1479
  this.ensurePersistentAgentSession(replacement);
1446
1480
  if (sessionKey) liveProviderSessions.add(sessionKey);
@@ -1716,6 +1750,7 @@ class AgentManager extends EventEmitter {
1716
1750
  if (!agent || !this.configManager || typeof this.configManager.ensureAgentSessionRecord !== 'function') {
1717
1751
  return '';
1718
1752
  }
1753
+ const previousPersistentSessionId = agent.persistentSessionId || '';
1719
1754
  const sessionOptions = agent.providerSessionKey
1720
1755
  ? this.acpSessionOptionsByKey.get(agent.providerSessionKey)
1721
1756
  : null;
@@ -1726,9 +1761,39 @@ class AgentManager extends EventEmitter {
1726
1761
  } : {}),
1727
1762
  ...patch,
1728
1763
  });
1729
- if (persistentSessionId && !agent.persistentSessionId) {
1764
+ if (persistentSessionId) {
1730
1765
  agent.persistentSessionId = persistentSessionId;
1731
1766
  }
1767
+ if (
1768
+ persistentSessionId
1769
+ && previousPersistentSessionId
1770
+ && persistentSessionId !== previousPersistentSessionId
1771
+ && agent.providerSessionKey
1772
+ && typeof this.configManager.getAgentSessionRecordForProviderSessionKey === 'function'
1773
+ ) {
1774
+ const record = this.configManager.getAgentSessionRecordForProviderSessionKey(agent.providerSessionKey);
1775
+ if (record) {
1776
+ agent.projectWorkspace = record.projectWorkspace || agent.projectWorkspace || '';
1777
+ agent.task = typeof record.task === 'string' ? record.task : (agent.task || '');
1778
+ agent.workflowTemplate = typeof record.workflowTemplate === 'string'
1779
+ ? record.workflowTemplate
1780
+ : (agent.workflowTemplate || '');
1781
+ agent.customTitle = typeof record.customTitle === 'string' ? record.customTitle : '';
1782
+ agent.pinned = record.pinned === true;
1783
+ agent.projectOrder = finiteOrder(record.projectOrder);
1784
+ agent.pinnedOrder = finiteOrder(record.pinnedOrder);
1785
+ agent.attentionSeq = finiteNonNegativeInteger(record.attentionSeq);
1786
+ agent.readAttentionSeq = finiteNonNegativeInteger(record.readAttentionSeq);
1787
+ agent.attentionUpdatedAt = finiteNumberOrNull(record.attentionUpdatedAt);
1788
+ agent.readAttentionAt = finiteNumberOrNull(record.readAttentionAt);
1789
+ agent.attentionReason = record.attentionReason || '';
1790
+ agent.attentionOutputEpoch = record.attentionOutputEpoch || '';
1791
+ agent.attentionOutputSeq = finiteNumberOrNull(record.attentionOutputSeq);
1792
+ agent.readOutputEpoch = record.readOutputEpoch || '';
1793
+ agent.readOutputSeq = finiteNumberOrNull(record.readOutputSeq);
1794
+ agent.unread = agentAttentionUnread(agent);
1795
+ }
1796
+ }
1732
1797
  return persistentSessionId;
1733
1798
  }
1734
1799
 
@@ -2264,6 +2329,8 @@ class AgentManager extends EventEmitter {
2264
2329
  this.resizeDrains.clear();
2265
2330
  this.inputQueues.clear();
2266
2331
  this.codexTerminalProfileQueues.clear();
2332
+ this.codexTerminalStartQueues.clear();
2333
+ this.codexTerminalStartOutput.clear();
2267
2334
  this.acpSessionOptionsByKey.clear();
2268
2335
  if (this.jsonCliRuntime) {
2269
2336
  for (const agentId of this.agents.keys()) this.jsonCliRuntime.unregisterAgent(agentId);
@@ -2373,6 +2440,37 @@ class AgentManager extends EventEmitter {
2373
2440
  throw new Error(`Terminal ${agentId} did not reach an exited state after kill`);
2374
2441
  }
2375
2442
 
2443
+ async waitForCodexTerminalStart(agentId) {
2444
+ const deadline = Date.now() + CODEX_TERMINAL_START_READY_TIMEOUT_MS;
2445
+ while (Date.now() <= deadline) {
2446
+ const agent = this.agents.get(agentId);
2447
+ if (!agent) throw new Error(`Codex Terminal ${agentId} disappeared during startup`);
2448
+ if (['dead', 'stopped'].includes(agent.status) || agent.engineStatus === 'exited') {
2449
+ const detail = trimSessionOutput(agent.previewText || agent.output || '').trim();
2450
+ throw new Error(detail || `Codex Terminal ${agentId} exited during startup`);
2451
+ }
2452
+ if ((this.codexTerminalStartOutput.get(agentId) || '').includes('\u001b')) {
2453
+ return;
2454
+ }
2455
+ await new Promise(resolve => setTimeout(resolve, CODEX_TERMINAL_START_READY_POLL_MS));
2456
+ }
2457
+ throw new Error(`Codex Terminal ${agentId} did not become ready within ${CODEX_TERMINAL_START_READY_TIMEOUT_MS}ms`);
2458
+ }
2459
+
2460
+ async enqueueCodexTerminalStart(providerHomeKey, operation) {
2461
+ const key = canonicalProviderHomePath(providerHomeKey || '.');
2462
+ const previous = this.codexTerminalStartQueues.get(key) || Promise.resolve();
2463
+ const next = previous.catch(() => {}).then(operation);
2464
+ this.codexTerminalStartQueues.set(key, next);
2465
+ try {
2466
+ return await next;
2467
+ } finally {
2468
+ if (this.codexTerminalStartQueues.get(key) === next) {
2469
+ this.codexTerminalStartQueues.delete(key);
2470
+ }
2471
+ }
2472
+ }
2473
+
2376
2474
  async startAgent(command, customWorkspace, callback, options = {}) {
2377
2475
  if (options.wantsMain !== false && options.skipRecoveryWait !== true) {
2378
2476
  await this.whenRecovered();
@@ -2774,19 +2872,19 @@ class AgentManager extends EventEmitter {
2774
2872
  shellLastCommandStartedAt: null,
2775
2873
  shellLastCommandFinishedAt: null,
2776
2874
  shellLastCommandDurationMs: null,
2777
- pinned: false,
2875
+ pinned: options.pinned === true,
2778
2876
  projectOrder: finiteOrder(options.projectOrder),
2779
2877
  pinnedOrder: finiteOrder(options.pinnedOrder),
2780
- attentionSeq: 0,
2781
- readAttentionSeq: 0,
2782
- attentionUpdatedAt: null,
2783
- readAttentionAt: null,
2784
- attentionReason: '',
2785
- attentionOutputEpoch: '',
2786
- attentionOutputSeq: null,
2787
- readOutputEpoch: '',
2788
- readOutputSeq: null,
2789
- unread: false,
2878
+ attentionSeq: finiteNonNegativeInteger(options.attentionSeq),
2879
+ readAttentionSeq: finiteNonNegativeInteger(options.readAttentionSeq),
2880
+ attentionUpdatedAt: finiteNumberOrNull(options.attentionUpdatedAt),
2881
+ readAttentionAt: finiteNumberOrNull(options.readAttentionAt),
2882
+ attentionReason: typeof options.attentionReason === 'string' ? options.attentionReason : '',
2883
+ attentionOutputEpoch: typeof options.attentionOutputEpoch === 'string' ? options.attentionOutputEpoch : '',
2884
+ attentionOutputSeq: finiteNumberOrNull(options.attentionOutputSeq),
2885
+ readOutputEpoch: typeof options.readOutputEpoch === 'string' ? options.readOutputEpoch : '',
2886
+ readOutputSeq: finiteNumberOrNull(options.readOutputSeq),
2887
+ unread: finiteNonNegativeInteger(options.attentionSeq) > finiteNonNegativeInteger(options.readAttentionSeq),
2790
2888
  archived: false,
2791
2889
  archivedAt: null,
2792
2890
  canForkNewWorktree: this.canCreateForkWorktree(projectWorkspace || workspace),
@@ -3016,18 +3114,41 @@ class AgentManager extends EventEmitter {
3016
3114
  }
3017
3115
 
3018
3116
  if (!useJsonCli && !useAcp) {
3019
- const engineLaunch = {
3020
- command: spawnProgram,
3021
- args,
3022
- cwd: workspace,
3023
- category: resolution.spec ? resolution.spec.category : 'shell',
3024
- reviveState: options.reviveTerminalState || null,
3117
+ const serializeCodexStartup = agentRecord.providerSessionProvider === 'codex'
3118
+ && resolution.engineName === 'native';
3119
+ const startTerminal = async () => {
3120
+ const engineLaunch = {
3121
+ command: spawnProgram,
3122
+ args,
3123
+ cwd: workspace,
3124
+ category: resolution.spec ? resolution.spec.category : 'shell',
3125
+ reviveState: options.reviveTerminalState || null,
3126
+ };
3127
+ if (serializeCodexStartup) this.codexTerminalStartOutput.set(agentId, '');
3128
+ try {
3129
+ await this.createAgentEngineSession(agentRecord, resolution.engine, engineLaunch);
3130
+ if (serializeCodexStartup) {
3131
+ await this.waitForCodexTerminalStart(agentId);
3132
+ }
3133
+ } finally {
3134
+ if (serializeCodexStartup) this.codexTerminalStartOutput.delete(agentId);
3135
+ }
3025
3136
  };
3026
- await this.createAgentEngineSession(agentRecord, resolution.engine, engineLaunch);
3137
+ if (serializeCodexStartup) {
3138
+ await this.enqueueCodexTerminalStart(
3139
+ providerHomePath || resolvedProviderHomeId || 'default',
3140
+ startTerminal
3141
+ );
3142
+ } else {
3143
+ await startTerminal();
3144
+ }
3027
3145
  }
3028
3146
  agentRecord.persistentSessionId = this.ensurePersistentAgentSession(agentRecord, {
3029
3147
  visibleOnMainPage: true,
3030
3148
  archived: false,
3149
+ ...(options.customTitleExplicit === true
3150
+ ? { customTitle: agentRecord.customTitle }
3151
+ : {}),
3031
3152
  });
3032
3153
 
3033
3154
  const agent = this.agents.get(agentId);
@@ -3643,7 +3764,7 @@ class AgentManager extends EventEmitter {
3643
3764
 
3644
3765
  const customTitle = String(title || '').trim().slice(0, 80);
3645
3766
  agent.customTitle = customTitle;
3646
- this.ensurePersistentAgentSession(agent);
3767
+ this.ensurePersistentAgentSession(agent, { customTitle });
3647
3768
  this.updateEngineProviderSessionMetadata(agent);
3648
3769
  this.emit('update');
3649
3770
  return { agentId, customTitle };
@@ -4305,24 +4426,33 @@ class AgentManager extends EventEmitter {
4305
4426
  }
4306
4427
 
4307
4428
  async rollbackPermanentWorktree(created) {
4308
- if (!created?.workspace || !created?.sourceWorkspace) return;
4429
+ if (!created?.workspace || !created?.sourceWorkspace) {
4430
+ return { rolledBack: true };
4431
+ }
4432
+ const errors = [];
4309
4433
  try {
4310
4434
  await execFileAsync('git', ['-C', created.sourceWorkspace, 'worktree', 'remove', '--force', created.workspace], {
4311
4435
  timeout: 60000,
4312
4436
  maxBuffer: 1024 * 1024 * 4,
4313
4437
  });
4314
- } catch {
4315
- // Rollback is best-effort so the original create/persist failure remains visible.
4438
+ } catch (error) {
4439
+ const message = error && error.stderr ? String(error.stderr).trim() : '';
4440
+ errors.push(message || 'Failed to remove the created worktree');
4316
4441
  }
4317
- if (!created.branch) return;
4318
- try {
4319
- await execFileAsync('git', ['-C', created.sourceWorkspace, 'branch', '-D', created.branch], {
4320
- timeout: 30000,
4321
- maxBuffer: 1024 * 1024 * 4,
4322
- });
4323
- } catch {
4324
- // The worktree removal may already have pruned the newly created branch.
4442
+ if (created.branch) {
4443
+ try {
4444
+ await execFileAsync('git', ['-C', created.sourceWorkspace, 'branch', '-D', created.branch], {
4445
+ timeout: 30000,
4446
+ maxBuffer: 1024 * 1024 * 4,
4447
+ });
4448
+ } catch (error) {
4449
+ const message = error && error.stderr ? String(error.stderr).trim() : '';
4450
+ errors.push(message || 'Failed to delete the created worktree branch');
4451
+ }
4325
4452
  }
4453
+ return errors.length > 0
4454
+ ? { rolledBack: false, error: errors.join('; ') }
4455
+ : { rolledBack: true };
4326
4456
  }
4327
4457
 
4328
4458
  async inspectForkWorktreeProject(workspace) {
@@ -4424,13 +4554,6 @@ class AgentManager extends EventEmitter {
4424
4554
  }
4425
4555
 
4426
4556
  const projectAgents = this.agentsForProjectWorkspace(inspected.workspace);
4427
- const removedMainPageSessionKeys = this.removeMainPageProviderSessionsForAgents(projectAgents);
4428
- const archivedAgentIds = [];
4429
- for (const agent of projectAgents) {
4430
- const result = await this.archiveAgent(agent.id);
4431
- if (result && !result.error) archivedAgentIds.push(agent.id);
4432
- }
4433
-
4434
4557
  const args = ['-C', inspected.workspace, 'worktree', 'remove'];
4435
4558
  if (options.force === true) args.push('--force');
4436
4559
  args.push(inspected.workspace);
@@ -4444,18 +4567,31 @@ class AgentManager extends EventEmitter {
4444
4567
  const message = error && error.stderr ? String(error.stderr).trim() : '';
4445
4568
  return {
4446
4569
  ...inspected,
4447
- archivedAgentIds,
4448
- removedMainPageSessionKeys,
4449
4570
  error: message || 'Failed to delete git worktree',
4450
4571
  };
4451
4572
  }
4452
4573
 
4574
+ const archivedAgentIds = [];
4575
+ const removedMainPageSessionKeys = [];
4576
+ const cleanupErrors = [];
4577
+ for (const agent of projectAgents) {
4578
+ const result = await this.archiveAgent(agent.id, { requireEngineExit: true });
4579
+ if (result && !result.error) {
4580
+ archivedAgentIds.push(agent.id);
4581
+ removedMainPageSessionKeys.push(...(result.removedMainPageSessionKeys || []));
4582
+ } else {
4583
+ cleanupErrors.push(`${agent.id}: ${result?.error || 'Failed to archive Agent'}`);
4584
+ }
4585
+ }
4453
4586
  return {
4454
4587
  workspace: inspected.workspace,
4455
4588
  deleted: true,
4456
4589
  forced: options.force === true,
4457
4590
  archivedAgentIds,
4458
- removedMainPageSessionKeys,
4591
+ removedMainPageSessionKeys: Array.from(new Set(removedMainPageSessionKeys)),
4592
+ ...(cleanupErrors.length > 0 ? {
4593
+ cleanupError: cleanupErrors.join('; '),
4594
+ } : {}),
4459
4595
  };
4460
4596
  }
4461
4597
 
@@ -4612,7 +4748,7 @@ class AgentManager extends EventEmitter {
4612
4748
  }
4613
4749
  }
4614
4750
 
4615
- async archiveAgent(agentId) {
4751
+ async archiveAgent(agentId, options = {}) {
4616
4752
  const agent = this.agents.get(agentId);
4617
4753
  if (!agent) {
4618
4754
  return { error: 'Agent not found' };
@@ -4621,13 +4757,26 @@ class AgentManager extends EventEmitter {
4621
4757
  return { error: 'Main Agent cannot be archived' };
4622
4758
  }
4623
4759
 
4760
+ if (options.requireEngineExit !== true) {
4761
+ const removedMainPageSessionKeys = this.removeMainPageProviderSessionsForAgents([agent]);
4762
+ this.ensurePersistentAgentSession(agent, { visibleOnMainPage: false });
4763
+ await this.killAgent(agentId, {
4764
+ reason: options.reason || 'manual-archive',
4765
+ recordHistory: options.recordHistory !== false && !isEphemeralShellAgent(agent),
4766
+ });
4767
+ if (options.scheduleProviderArchive !== false) this.scheduleCodexSessionArchive(agent);
4768
+ return { agentId, archived: true, removed: true, removedMainPageSessionKeys };
4769
+ }
4770
+
4771
+ const killResult = await this.killAgent(agentId, {
4772
+ reason: options.reason || 'manual-archive',
4773
+ recordHistory: options.recordHistory !== false && !isEphemeralShellAgent(agent),
4774
+ requireEngineExit: true,
4775
+ });
4776
+ if (killResult?.error) return killResult;
4624
4777
  const removedMainPageSessionKeys = this.removeMainPageProviderSessionsForAgents([agent]);
4625
4778
  this.ensurePersistentAgentSession(agent, { visibleOnMainPage: false });
4626
- await this.killAgent(agentId, {
4627
- reason: 'manual-archive',
4628
- recordHistory: !isEphemeralShellAgent(agent),
4629
- });
4630
- this.scheduleCodexSessionArchive(agent);
4779
+ if (options.scheduleProviderArchive !== false) this.scheduleCodexSessionArchive(agent);
4631
4780
  return { agentId, archived: true, removed: true, removedMainPageSessionKeys };
4632
4781
  }
4633
4782
 
@@ -4662,15 +4811,42 @@ class AgentManager extends EventEmitter {
4662
4811
 
4663
4812
  async killAgent(agentId, options = {}) {
4664
4813
  const agent = this.agents.get(agentId);
4665
- if (!agent) return;
4814
+ if (!agent) return { agentId, killed: true, missing: true };
4666
4815
 
4816
+ const engine = this.engineBridge.getEngine(agent.engineName);
4817
+ if (options.requireEngineExit === true && !engine) {
4818
+ return { agentId, error: 'Agent runtime is unavailable; process exit cannot be verified' };
4819
+ }
4667
4820
  try {
4668
- const engine = this.engineBridge.getEngine(agent.engineName);
4669
4821
  if (engine) {
4670
4822
  await engine.killSession(agentId);
4671
4823
  }
4672
4824
  } catch (error) {
4673
4825
  console.error('Failed to kill agent:', error);
4826
+ if (options.requireEngineExit === true) {
4827
+ return { agentId, error: error.message || 'Failed to stop Agent runtime' };
4828
+ }
4829
+ }
4830
+
4831
+ if (options.requireEngineExit === true && engine) {
4832
+ const deadline = Date.now() + 3000;
4833
+ let lastState = null;
4834
+ while (Date.now() < deadline) {
4835
+ try {
4836
+ lastState = await engine.getSessionState(agentId);
4837
+ } catch (error) {
4838
+ if (isSessionNotAvailableError(error)) {
4839
+ lastState = null;
4840
+ break;
4841
+ }
4842
+ return { agentId, error: error.message || 'Failed to verify Agent process exit' };
4843
+ }
4844
+ if (!isLiveEngineSessionState(lastState)) break;
4845
+ await new Promise(resolve => setTimeout(resolve, 50));
4846
+ }
4847
+ if (isLiveEngineSessionState(lastState)) {
4848
+ return { agentId, error: 'Agent process did not exit within 3 seconds' };
4849
+ }
4674
4850
  }
4675
4851
 
4676
4852
  if (options.recordHistory !== false && !isEphemeralShellAgent(agent)) {
@@ -4697,6 +4873,7 @@ class AgentManager extends EventEmitter {
4697
4873
  if (options.emitUpdate !== false) {
4698
4874
  this.emit('update');
4699
4875
  }
4876
+ return { agentId, killed: true };
4700
4877
  }
4701
4878
 
4702
4879
  async getAgentSessionText(agentId) {
@@ -0,0 +1,109 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const { spawn } = require('child_process');
4
+
5
+ function writeJsonAtomic(filePath, value) {
6
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
7
+ const temporaryPath = `${filePath}.${process.pid}.tmp`;
8
+ fs.writeFileSync(temporaryPath, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
9
+ fs.renameSync(temporaryPath, filePath);
10
+ }
11
+
12
+ function appendLog(logPath, message) {
13
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
14
+ fs.appendFileSync(logPath, `[${new Date().toISOString()}] ${message}\n`);
15
+ }
16
+
17
+ function validatePayload(payload) {
18
+ if (!payload || typeof payload !== 'object') throw new Error('Missing bundle update payload');
19
+ for (const key of ['stateFile', 'logPath', 'releaseDir', 'installer']) {
20
+ if (!path.isAbsolute(String(payload[key] || ''))) throw new Error(`Invalid bundle update ${key}`);
21
+ }
22
+ if (!/^[0-9A-Za-z.+-]+$/.test(String(payload.version || ''))) {
23
+ throw new Error('Invalid bundle update version');
24
+ }
25
+ return payload;
26
+ }
27
+
28
+ function stateFor(payload, phase, extra = {}) {
29
+ return {
30
+ method: payload.method,
31
+ targetMethod: payload.targetMethod,
32
+ phase,
33
+ version: payload.version,
34
+ previousVersion: payload.previousVersion,
35
+ startedAt: payload.startedAt,
36
+ logPath: payload.logPath,
37
+ ...extra,
38
+ };
39
+ }
40
+
41
+ function commandEnvironment() {
42
+ const env = { ...process.env };
43
+ delete env.FARMING_BUNDLE_UPDATE_PAYLOAD;
44
+ delete env.FARMING_RUN_SERVER;
45
+ delete env.FARMING_RUN_NATIVE_PTY_HOST;
46
+ return env;
47
+ }
48
+
49
+ function runInstaller(payload) {
50
+ return new Promise((resolve, reject) => {
51
+ const logFd = fs.openSync(payload.logPath, 'a');
52
+ const child = spawn('bash', [payload.installer, 'install'], {
53
+ cwd: payload.releaseDir,
54
+ env: commandEnvironment(),
55
+ stdio: ['ignore', logFd, logFd],
56
+ });
57
+ child.once('error', error => {
58
+ fs.closeSync(logFd);
59
+ reject(error);
60
+ });
61
+ child.once('exit', (code, signal) => {
62
+ fs.closeSync(logFd);
63
+ if (code === 0) {
64
+ resolve();
65
+ return;
66
+ }
67
+ reject(new Error(`bundle installer exited with ${signal || code}`));
68
+ });
69
+ });
70
+ }
71
+
72
+ async function runBundleUpdate(rawPayload) {
73
+ const payload = validatePayload(rawPayload);
74
+ writeJsonAtomic(payload.stateFile, stateFor(payload, 'installing'));
75
+ await new Promise(resolve => setTimeout(resolve, 1_000));
76
+ try {
77
+ await runInstaller(payload);
78
+ writeJsonAtomic(payload.stateFile, stateFor(payload, 'succeeded', {
79
+ completedAt: new Date().toISOString(),
80
+ }));
81
+ appendLog(payload.logPath, `Farming updated to ${payload.version}`);
82
+ } catch (error) {
83
+ const message = error && error.message ? error.message : String(error);
84
+ writeJsonAtomic(payload.stateFile, stateFor(payload, 'failed', {
85
+ error: message,
86
+ completedAt: new Date().toISOString(),
87
+ }));
88
+ appendLog(payload.logPath, `Update failed: ${message}`);
89
+ }
90
+ }
91
+
92
+ if (require.main === module) {
93
+ let payload;
94
+ try {
95
+ payload = JSON.parse(process.env.FARMING_BUNDLE_UPDATE_PAYLOAD || '');
96
+ } catch {
97
+ console.error('Invalid FARMING_BUNDLE_UPDATE_PAYLOAD');
98
+ process.exit(1);
99
+ }
100
+ runBundleUpdate(payload).catch(error => {
101
+ console.error(error.message || error);
102
+ process.exit(1);
103
+ });
104
+ }
105
+
106
+ module.exports = {
107
+ runBundleUpdate,
108
+ validatePayload,
109
+ };