farming-code 2.2.8 → 2.2.12
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 +42 -28
- package/README.zh_cn.md +42 -28
- 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 +1592 -741
- 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 +54 -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 +35 -0
- package/backend/control-api.js +192 -17
- package/backend/executable-discovery.js +2 -0
- 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 +354 -84
- 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 +176 -17
- package/backend/workspace-directory.js +232 -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-DWsa7DXG.js +206 -0
- package/dist/assets/{FileEditorMarkdownPreview-elKWc8Im.js → FileEditorMarkdownPreview-UlVd0O4D.js} +92 -92
- package/dist/assets/FileEditorPane-Dxx4rKOg.js +2 -0
- package/dist/assets/IconGlyphs-AU22gPDY.js +1 -0
- package/dist/assets/ProjectFilesSection-DE7Nb9Jk.js +12 -0
- package/dist/assets/ReviewPage-CeDKDyZh.js +3 -0
- package/dist/assets/code-dark-DPiuyeSp.css +1 -0
- package/dist/assets/file-icons-Bw2qd5iT.js +1 -0
- package/dist/assets/{index-BrbljRqn.js → index-CUFPJoa5.js} +3 -3
- package/dist/assets/main-Bqtb5kuz.css +1 -0
- package/dist/assets/{monaco.contribution-CyLosfZI.js → monaco.contribution-BXz-lfFB.js} +2 -2
- package/dist/assets/{tsMode-DGZTlTj8.js → tsMode-B0oUTcXQ.js} +1 -1
- package/dist/assets/workspace-editor-model-BQol4qbA.js +1 -0
- package/dist/assets/workspace-editor-monaco-BICJESG0.js +4 -0
- package/dist/assets/workspace-editor-monaco-DRJ7IaXk.js +1 -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/farming-2/site.webmanifest +2 -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 +1978 -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-8dYAM6ql.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/ReviewPage-BaXu1ZdX.js +0 -3
- package/dist/assets/code-dark-CDkOQAtK.css +0 -1
- package/dist/assets/file-icons-EFUGSSwf.js +0 -1
- package/dist/assets/main-D073SnW4.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
|
@@ -14,10 +14,15 @@ const DEFAULT_SEARCH_LIMIT = 100;
|
|
|
14
14
|
const DEFAULT_GIT_CHANGES_LIMIT = 500;
|
|
15
15
|
const DEFAULT_GIT_STATUS_CACHE_TTL_MS = 30000;
|
|
16
16
|
const DEFAULT_GIT_STATUS_INLINE_TIMEOUT_MS = 80;
|
|
17
|
+
const DEFAULT_GIT_STATUS_TIMEOUT_MS = 15000;
|
|
17
18
|
const DEFAULT_SEARCH_TIMEOUT_MS = 3000;
|
|
18
19
|
const DEFAULT_BLAME_TIMEOUT_MS = 5000;
|
|
19
20
|
const DEFAULT_DIFF_TIMEOUT_MS = 5000;
|
|
20
21
|
const DEFAULT_DIFF_MAX_BUFFER = 1024 * 1024;
|
|
22
|
+
const DEFAULT_GIT_HISTORY_LIMIT = 50;
|
|
23
|
+
const MAX_GIT_HISTORY_LIMIT = 100;
|
|
24
|
+
const DEFAULT_GIT_HISTORY_TIMEOUT_MS = 5000;
|
|
25
|
+
const DEFAULT_GIT_HISTORY_MAX_BUFFER = 8 * 1024 * 1024;
|
|
21
26
|
const DEFAULT_WATCH_DEPTH = 1;
|
|
22
27
|
const SEARCH_FILE_LIST_MAX_BUFFER = 16 * 1024 * 1024;
|
|
23
28
|
const BINARY_SNIFF_BYTES = 8192;
|
|
@@ -389,6 +394,7 @@ function sha1(buffer) {
|
|
|
389
394
|
|
|
390
395
|
function previewForPath(filePath, options = {}) {
|
|
391
396
|
const extension = path.extname(filePath).toLowerCase();
|
|
397
|
+
if (extension === '.pdf') return { kind: 'pdf', mediaType: 'application/pdf' };
|
|
392
398
|
if (!options.includeTextImages && TEXT_IMAGE_PREVIEW_EXTENSIONS.has(extension)) return null;
|
|
393
399
|
const mediaType = IMAGE_PREVIEW_MEDIA_TYPES.get(extension);
|
|
394
400
|
return mediaType ? { kind: 'image', mediaType } : null;
|
|
@@ -509,6 +515,132 @@ function gitStatusReviewRank(kind) {
|
|
|
509
515
|
return 6;
|
|
510
516
|
}
|
|
511
517
|
|
|
518
|
+
function normalizeGitHistoryLimit(value) {
|
|
519
|
+
const parsed = Number(value);
|
|
520
|
+
if (!Number.isInteger(parsed) || parsed < 1) return DEFAULT_GIT_HISTORY_LIMIT;
|
|
521
|
+
return Math.min(parsed, MAX_GIT_HISTORY_LIMIT);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function normalizeGitHistorySkip(value) {
|
|
525
|
+
const parsed = Number(value);
|
|
526
|
+
if (!Number.isInteger(parsed) || parsed < 0) return 0;
|
|
527
|
+
return Math.min(parsed, 1_000_000);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function normalizeGitObjectId(value, fieldName = 'commit') {
|
|
531
|
+
const normalized = String(value || '').trim();
|
|
532
|
+
if (!/^[0-9a-f]{40,64}$/i.test(normalized)) {
|
|
533
|
+
throw new WorkspaceFileError(`${fieldName} must be a full git object id`, 400);
|
|
534
|
+
}
|
|
535
|
+
return normalized.toLowerCase();
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function gitEmptyTreeObjectId(objectFormat) {
|
|
539
|
+
const algorithm = objectFormat === 'sha256' ? 'sha256' : 'sha1';
|
|
540
|
+
return crypto.createHash(algorithm).update(Buffer.from('tree 0\0')).digest('hex');
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function gitHistoryReference(value) {
|
|
544
|
+
let id = String(value || '').trim();
|
|
545
|
+
if (!id) return null;
|
|
546
|
+
if (id.startsWith('tag: ')) id = id.slice(5);
|
|
547
|
+
if (id === 'HEAD') return { id, name: id, category: 'head' };
|
|
548
|
+
if (id.startsWith('refs/heads/')) {
|
|
549
|
+
return { id, name: id.slice('refs/heads/'.length), category: 'local-branch' };
|
|
550
|
+
}
|
|
551
|
+
if (id.startsWith('refs/remotes/')) {
|
|
552
|
+
if (/\/HEAD$/.test(id)) return null;
|
|
553
|
+
return { id, name: id.slice('refs/remotes/'.length), category: 'remote-branch' };
|
|
554
|
+
}
|
|
555
|
+
if (id.startsWith('refs/tags/')) {
|
|
556
|
+
return { id, name: id.slice('refs/tags/'.length), category: 'tag' };
|
|
557
|
+
}
|
|
558
|
+
return { id, name: id.replace(/^refs\//, ''), category: 'reference' };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
function parseGitHistoryReferences(value) {
|
|
562
|
+
const references = [];
|
|
563
|
+
const seen = new Set();
|
|
564
|
+
const add = candidate => {
|
|
565
|
+
const reference = gitHistoryReference(candidate);
|
|
566
|
+
if (!reference || seen.has(reference.id)) return;
|
|
567
|
+
seen.add(reference.id);
|
|
568
|
+
references.push(reference);
|
|
569
|
+
};
|
|
570
|
+
|
|
571
|
+
String(value || '').split(', ').forEach(decoration => {
|
|
572
|
+
const arrowIndex = decoration.indexOf(' -> ');
|
|
573
|
+
if (arrowIndex === -1) {
|
|
574
|
+
add(decoration);
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
add(decoration.slice(0, arrowIndex));
|
|
578
|
+
add(decoration.slice(arrowIndex + 4));
|
|
579
|
+
});
|
|
580
|
+
return references;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function parseGitHistoryLog(stdout) {
|
|
584
|
+
const fields = String(stdout || '').split('\0');
|
|
585
|
+
const items = [];
|
|
586
|
+
for (let index = 0; index + 7 < fields.length; index += 8) {
|
|
587
|
+
const id = fields[index];
|
|
588
|
+
if (!id) continue;
|
|
589
|
+
const timestamp = Date.parse(fields[index + 4]);
|
|
590
|
+
items.push({
|
|
591
|
+
id,
|
|
592
|
+
displayId: id.slice(0, 8),
|
|
593
|
+
parentIds: fields[index + 1].split(' ').filter(Boolean),
|
|
594
|
+
author: fields[index + 2],
|
|
595
|
+
authorEmail: fields[index + 3],
|
|
596
|
+
timestamp: Number.isFinite(timestamp) ? timestamp : undefined,
|
|
597
|
+
subject: fields[index + 5],
|
|
598
|
+
message: String(fields[index + 6] || '').trimEnd(),
|
|
599
|
+
references: parseGitHistoryReferences(fields[index + 7]),
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
return items;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function gitHistoryChangeKind(statusCode) {
|
|
606
|
+
if (statusCode.startsWith('A')) return 'added';
|
|
607
|
+
if (statusCode.startsWith('D')) return 'deleted';
|
|
608
|
+
if (statusCode.startsWith('R')) return 'renamed';
|
|
609
|
+
if (statusCode.startsWith('C')) return 'copied';
|
|
610
|
+
if (statusCode.startsWith('T')) return 'type-changed';
|
|
611
|
+
return 'modified';
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function gitHistoryChangeLabel(kind) {
|
|
615
|
+
if (kind === 'added') return 'A';
|
|
616
|
+
if (kind === 'deleted') return 'D';
|
|
617
|
+
if (kind === 'renamed') return 'R';
|
|
618
|
+
if (kind === 'copied') return 'C';
|
|
619
|
+
if (kind === 'type-changed') return 'T';
|
|
620
|
+
return 'M';
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function parseGitHistoryChanges(stdout) {
|
|
624
|
+
const fields = String(stdout || '').split('\0');
|
|
625
|
+
const items = [];
|
|
626
|
+
for (let index = 0; index < fields.length;) {
|
|
627
|
+
const statusCode = fields[index++];
|
|
628
|
+
if (!statusCode) continue;
|
|
629
|
+
const kind = gitHistoryChangeKind(statusCode);
|
|
630
|
+
if (kind === 'renamed' || kind === 'copied') {
|
|
631
|
+
const previousPath = fields[index++] || '';
|
|
632
|
+
const filePath = fields[index++] || '';
|
|
633
|
+
if (!filePath) continue;
|
|
634
|
+
items.push({ path: filePath, previousPath, status: kind, statusLabel: gitHistoryChangeLabel(kind) });
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
const filePath = fields[index++] || '';
|
|
638
|
+
if (!filePath) continue;
|
|
639
|
+
items.push({ path: filePath, status: kind, statusLabel: gitHistoryChangeLabel(kind) });
|
|
640
|
+
}
|
|
641
|
+
return items;
|
|
642
|
+
}
|
|
643
|
+
|
|
512
644
|
async function workspaceEntryTypeForGitChange(root, filePath) {
|
|
513
645
|
try {
|
|
514
646
|
const stat = await fsp.lstat(path.join(root, filePath.replace(/\/+$/, '')));
|
|
@@ -978,6 +1110,8 @@ class WorkspaceFileService {
|
|
|
978
1110
|
this.blameTimeoutMs = options.blameTimeoutMs ?? DEFAULT_BLAME_TIMEOUT_MS;
|
|
979
1111
|
this.diffTimeoutMs = options.diffTimeoutMs ?? DEFAULT_DIFF_TIMEOUT_MS;
|
|
980
1112
|
this.diffMaxBuffer = options.diffMaxBuffer ?? DEFAULT_DIFF_MAX_BUFFER;
|
|
1113
|
+
this.gitHistoryTimeoutMs = options.gitHistoryTimeoutMs ?? DEFAULT_GIT_HISTORY_TIMEOUT_MS;
|
|
1114
|
+
this.gitHistoryMaxBuffer = options.gitHistoryMaxBuffer ?? DEFAULT_GIT_HISTORY_MAX_BUFFER;
|
|
981
1115
|
const bundledRipgrepPath = resolveBundledRipgrepPath();
|
|
982
1116
|
this.rgPath = options.rgPath || process.env.FARMING_RG_BIN || bundledRipgrepPath || 'rg';
|
|
983
1117
|
this.rgFallbackPath = options.rgFallbackPath ?? (this.rgPath === 'rg' ? bundledRipgrepPath : 'rg');
|
|
@@ -986,6 +1120,7 @@ class WorkspaceFileService {
|
|
|
986
1120
|
this.gitPath = options.gitPath || 'git';
|
|
987
1121
|
this.gitStatusCacheTtlMs = options.gitStatusCacheTtlMs ?? DEFAULT_GIT_STATUS_CACHE_TTL_MS;
|
|
988
1122
|
this.gitStatusInlineTimeoutMs = options.gitStatusInlineTimeoutMs ?? DEFAULT_GIT_STATUS_INLINE_TIMEOUT_MS;
|
|
1123
|
+
this.gitStatusTimeoutMs = options.gitStatusTimeoutMs ?? DEFAULT_GIT_STATUS_TIMEOUT_MS;
|
|
989
1124
|
this.gitStatusCache = new Map();
|
|
990
1125
|
this.watchers = new Map();
|
|
991
1126
|
this.watchOptions = options.watchOptions || {};
|
|
@@ -1588,11 +1723,31 @@ class WorkspaceFileService {
|
|
|
1588
1723
|
`--untracked-files=${untrackedFiles}`,
|
|
1589
1724
|
'--ignored=no',
|
|
1590
1725
|
...gitStatusExcludePathspecArgs(),
|
|
1591
|
-
], { cwd: root });
|
|
1726
|
+
], { cwd: root, timeout: options.timeoutMs ?? this.gitStatusTimeoutMs });
|
|
1592
1727
|
return parseGitStatus(stdout);
|
|
1593
1728
|
} catch (error) {
|
|
1594
|
-
if (error.code === '
|
|
1729
|
+
if (error.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER' && options.allowPartial === true) {
|
|
1730
|
+
const partialOutput = String(error.stdout || '');
|
|
1731
|
+
const lastCompleteRecord = partialOutput.lastIndexOf('\0');
|
|
1732
|
+
const statusByPath = parseGitStatus(lastCompleteRecord >= 0
|
|
1733
|
+
? partialOutput.slice(0, lastCompleteRecord + 1)
|
|
1734
|
+
: '');
|
|
1735
|
+
statusByPath.truncated = true;
|
|
1736
|
+
return statusByPath;
|
|
1737
|
+
}
|
|
1738
|
+
if (error.code === 'ENOENT') {
|
|
1739
|
+
if (options.throwOnError === true) {
|
|
1740
|
+
throw new WorkspaceFileError('git is not installed', 501);
|
|
1741
|
+
}
|
|
1742
|
+
return new Map();
|
|
1743
|
+
}
|
|
1744
|
+
if (error.code === 'ETIMEDOUT' || error.signal === 'SIGTERM') {
|
|
1745
|
+
throw new WorkspaceFileError('git status timed out', 504);
|
|
1746
|
+
}
|
|
1595
1747
|
if (error.stderr && /not a git repository/i.test(error.stderr)) return new Map();
|
|
1748
|
+
if (options.throwOnError === true) {
|
|
1749
|
+
throw new WorkspaceFileError(error.stderr || 'git status failed', 500);
|
|
1750
|
+
}
|
|
1596
1751
|
return new Map();
|
|
1597
1752
|
}
|
|
1598
1753
|
}
|
|
@@ -1719,10 +1874,165 @@ class WorkspaceFileService {
|
|
|
1719
1874
|
}
|
|
1720
1875
|
}
|
|
1721
1876
|
|
|
1877
|
+
async gitHistory(workspaceRoot, options = {}) {
|
|
1878
|
+
const root = await this.resolveRoot(workspaceRoot);
|
|
1879
|
+
const limit = normalizeGitHistoryLimit(options.limit);
|
|
1880
|
+
const skip = normalizeGitHistorySkip(options.skip);
|
|
1881
|
+
const scope = options.scope === 'all' ? 'all' : 'current';
|
|
1882
|
+
let branch = '';
|
|
1883
|
+
try {
|
|
1884
|
+
branch = await this.gitBranch(root);
|
|
1885
|
+
await this.execFile(this.gitPath, ['-C', root, 'rev-parse', '--is-inside-work-tree'], {
|
|
1886
|
+
timeout: 1500,
|
|
1887
|
+
maxBuffer: 64 * 1024,
|
|
1888
|
+
});
|
|
1889
|
+
} catch (error) {
|
|
1890
|
+
if (error.code === 'ENOENT') throw new WorkspaceFileError('git is not installed', 501);
|
|
1891
|
+
if (error.stderr && /not a git repository/i.test(String(error.stderr))) {
|
|
1892
|
+
return { isGitRepo: false, branch: '', head: '', scope, items: [], hasMore: false, nextSkip: null };
|
|
1893
|
+
}
|
|
1894
|
+
throw new WorkspaceFileError(error.stderr || 'git history failed', 500);
|
|
1895
|
+
}
|
|
1896
|
+
|
|
1897
|
+
let head = '';
|
|
1898
|
+
try {
|
|
1899
|
+
const result = await this.execFile(this.gitPath, ['-C', root, 'rev-parse', '--verify', 'HEAD'], {
|
|
1900
|
+
timeout: 1500,
|
|
1901
|
+
maxBuffer: 64 * 1024,
|
|
1902
|
+
});
|
|
1903
|
+
head = String(result.stdout || '').trim();
|
|
1904
|
+
} catch (error) {
|
|
1905
|
+
if (error.stderr && /(needed a single revision|unknown revision|bad revision|ambiguous argument)/i.test(String(error.stderr))) {
|
|
1906
|
+
return { isGitRepo: true, branch, head: '', scope, items: [], hasMore: false, nextSkip: null };
|
|
1907
|
+
}
|
|
1908
|
+
throw new WorkspaceFileError(error.stderr || 'git history failed', 500);
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
try {
|
|
1912
|
+
const revisionArgs = scope === 'all'
|
|
1913
|
+
? ['HEAD', '--branches', '--tags', '--remotes']
|
|
1914
|
+
: ['--first-parent', 'HEAD'];
|
|
1915
|
+
const { stdout } = await this.execFile(this.gitPath, [
|
|
1916
|
+
'-C', root,
|
|
1917
|
+
'log',
|
|
1918
|
+
'-z',
|
|
1919
|
+
'--date-order',
|
|
1920
|
+
'--decorate=full',
|
|
1921
|
+
'--format=%H%x00%P%x00%an%x00%ae%x00%aI%x00%s%x00%B%x00%D',
|
|
1922
|
+
`--max-count=${limit + 1}`,
|
|
1923
|
+
`--skip=${skip}`,
|
|
1924
|
+
...revisionArgs,
|
|
1925
|
+
], {
|
|
1926
|
+
timeout: this.gitHistoryTimeoutMs,
|
|
1927
|
+
maxBuffer: this.gitHistoryMaxBuffer,
|
|
1928
|
+
});
|
|
1929
|
+
const parsed = parseGitHistoryLog(stdout);
|
|
1930
|
+
const hasMore = parsed.length > limit;
|
|
1931
|
+
const items = parsed.slice(0, limit);
|
|
1932
|
+
return {
|
|
1933
|
+
isGitRepo: true,
|
|
1934
|
+
branch,
|
|
1935
|
+
head,
|
|
1936
|
+
scope,
|
|
1937
|
+
items,
|
|
1938
|
+
hasMore,
|
|
1939
|
+
nextSkip: hasMore ? skip + items.length : null,
|
|
1940
|
+
};
|
|
1941
|
+
} catch (error) {
|
|
1942
|
+
if (error.code === 'ENOENT') throw new WorkspaceFileError('git is not installed', 501);
|
|
1943
|
+
if (error.code === 'ETIMEDOUT' || error.signal === 'SIGTERM') {
|
|
1944
|
+
throw new WorkspaceFileError('git history timed out', 504);
|
|
1945
|
+
}
|
|
1946
|
+
if (error.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER') {
|
|
1947
|
+
throw new WorkspaceFileError('git history output is too large', 413);
|
|
1948
|
+
}
|
|
1949
|
+
if (error.stderr && /not a git repository/i.test(String(error.stderr))) {
|
|
1950
|
+
return { isGitRepo: false, branch: '', head: '', scope, items: [], hasMore: false, nextSkip: null };
|
|
1951
|
+
}
|
|
1952
|
+
throw new WorkspaceFileError(error.stderr || 'git history failed', 500);
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
async gitHistoryChanges(workspaceRoot, commitValue, parentValue, options = {}) {
|
|
1957
|
+
const root = await this.resolveRoot(workspaceRoot);
|
|
1958
|
+
const commit = normalizeGitObjectId(commitValue);
|
|
1959
|
+
const requestedParent = parentValue ? normalizeGitObjectId(parentValue, 'parent') : '';
|
|
1960
|
+
const limit = Math.max(1, Math.min(2000, Number(options.limit) || DEFAULT_GIT_CHANGES_LIMIT));
|
|
1961
|
+
|
|
1962
|
+
try {
|
|
1963
|
+
const { stdout: parentOutput } = await this.execFile(this.gitPath, [
|
|
1964
|
+
'-C', root, 'rev-list', '--parents', '-n', '1', commit,
|
|
1965
|
+
], {
|
|
1966
|
+
timeout: this.gitHistoryTimeoutMs,
|
|
1967
|
+
maxBuffer: 64 * 1024,
|
|
1968
|
+
});
|
|
1969
|
+
const revision = String(parentOutput || '').trim().split(/\s+/).filter(Boolean);
|
|
1970
|
+
if (!revision.length || revision[0].toLowerCase() !== commit) {
|
|
1971
|
+
throw new WorkspaceFileError('commit was not found', 404);
|
|
1972
|
+
}
|
|
1973
|
+
const parentIds = revision.slice(1).map(value => value.toLowerCase());
|
|
1974
|
+
if (requestedParent && !parentIds.includes(requestedParent)) {
|
|
1975
|
+
throw new WorkspaceFileError('parent is not a parent of commit', 400);
|
|
1976
|
+
}
|
|
1977
|
+
const parent = requestedParent || parentIds[0] || '';
|
|
1978
|
+
let comparisonBase = parent;
|
|
1979
|
+
if (!comparisonBase) {
|
|
1980
|
+
let objectFormat = 'sha1';
|
|
1981
|
+
try {
|
|
1982
|
+
const result = await this.execFile(this.gitPath, ['-C', root, 'rev-parse', '--show-object-format'], {
|
|
1983
|
+
timeout: 1500,
|
|
1984
|
+
maxBuffer: 64 * 1024,
|
|
1985
|
+
});
|
|
1986
|
+
objectFormat = String(result.stdout || '').trim();
|
|
1987
|
+
} catch {
|
|
1988
|
+
// Git versions without --show-object-format use SHA-1 repositories.
|
|
1989
|
+
}
|
|
1990
|
+
comparisonBase = gitEmptyTreeObjectId(objectFormat);
|
|
1991
|
+
}
|
|
1992
|
+
const args = parent
|
|
1993
|
+
? ['-C', root, 'diff', '--name-status', '-z', '-M', parent, commit, '--']
|
|
1994
|
+
: ['-C', root, 'diff-tree', '--root', '--no-commit-id', '--name-status', '-r', '-z', '-M', commit];
|
|
1995
|
+
const { stdout } = await this.execFile(this.gitPath, args, {
|
|
1996
|
+
timeout: this.gitHistoryTimeoutMs,
|
|
1997
|
+
maxBuffer: this.gitHistoryMaxBuffer,
|
|
1998
|
+
});
|
|
1999
|
+
const allItems = parseGitHistoryChanges(stdout);
|
|
2000
|
+
return {
|
|
2001
|
+
commit,
|
|
2002
|
+
comparisonBase,
|
|
2003
|
+
parent: parent || null,
|
|
2004
|
+
parentIds,
|
|
2005
|
+
items: allItems.slice(0, limit),
|
|
2006
|
+
truncated: allItems.length > limit,
|
|
2007
|
+
};
|
|
2008
|
+
} catch (error) {
|
|
2009
|
+
if (error instanceof WorkspaceFileError) throw error;
|
|
2010
|
+
if (error.code === 'ENOENT') throw new WorkspaceFileError('git is not installed', 501);
|
|
2011
|
+
if (error.code === 'ETIMEDOUT' || error.signal === 'SIGTERM') {
|
|
2012
|
+
throw new WorkspaceFileError('git commit changes timed out', 504);
|
|
2013
|
+
}
|
|
2014
|
+
if (error.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER') {
|
|
2015
|
+
throw new WorkspaceFileError('git commit changes output is too large', 413);
|
|
2016
|
+
}
|
|
2017
|
+
if (error.stderr && /(bad object|unknown revision|ambiguous argument|not a valid object name)/i.test(String(error.stderr))) {
|
|
2018
|
+
throw new WorkspaceFileError('commit was not found', 404);
|
|
2019
|
+
}
|
|
2020
|
+
if (error.stderr && /not a git repository/i.test(String(error.stderr))) {
|
|
2021
|
+
throw new WorkspaceFileError('workspace is not a git repository', 409);
|
|
2022
|
+
}
|
|
2023
|
+
throw new WorkspaceFileError(error.stderr || 'git commit changes failed', 500);
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
|
|
1722
2027
|
async changes(workspaceRoot, options = {}) {
|
|
1723
2028
|
const root = await this.resolveRoot(workspaceRoot);
|
|
1724
2029
|
const limit = Math.max(1, Math.min(2000, Number(options.limit) || DEFAULT_GIT_CHANGES_LIMIT));
|
|
1725
|
-
const gitStatusByPath = await this.loadGitStatusByPath(root, {
|
|
2030
|
+
const gitStatusByPath = await this.loadGitStatusByPath(root, {
|
|
2031
|
+
allowPartial: true,
|
|
2032
|
+
throwOnError: true,
|
|
2033
|
+
untrackedFiles: 'all',
|
|
2034
|
+
});
|
|
2035
|
+
this.invalidateGitStatus(root);
|
|
1726
2036
|
const allItems = await Promise.all(Array.from(gitStatusByPath.entries())
|
|
1727
2037
|
.map(async ([filePath, status]) => ({
|
|
1728
2038
|
path: filePath,
|
|
@@ -1740,7 +2050,7 @@ class WorkspaceFileService {
|
|
|
1740
2050
|
|
|
1741
2051
|
return {
|
|
1742
2052
|
items: allItems.slice(0, limit),
|
|
1743
|
-
truncated: allItems.length > limit,
|
|
2053
|
+
truncated: gitStatusByPath.truncated === true || allItems.length > limit,
|
|
1744
2054
|
};
|
|
1745
2055
|
}
|
|
1746
2056
|
|
|
@@ -2762,4 +3072,9 @@ module.exports = {
|
|
|
2762
3072
|
parseUnifiedDiffHunks,
|
|
2763
3073
|
selectUnifiedDiffHunk,
|
|
2764
3074
|
resolveCommandRunnerNodePath,
|
|
3075
|
+
normalizeGitHistoryLimit,
|
|
3076
|
+
normalizeGitHistorySkip,
|
|
3077
|
+
parseGitHistoryChanges,
|
|
3078
|
+
parseGitHistoryLog,
|
|
3079
|
+
parseGitHistoryReferences,
|
|
2765
3080
|
};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { WorkspaceFileError } = require('./workspace-file-service');
|
|
5
|
+
|
|
6
|
+
const GLOBAL_WORKSPACE_FILES_AGENT_ID = '__farming_global_files__';
|
|
7
|
+
const GLOBAL_WORKSPACE_ROOT_ID = 'wroot_global';
|
|
8
|
+
const GLOBAL_WORKSPACE_FILES_ROOT = '/';
|
|
9
|
+
const PROJECT_FILES_WORKSPACE_PREFIX = '__farming_project__:';
|
|
10
|
+
|
|
11
|
+
function normalizeWorkspacePath(value) {
|
|
12
|
+
const raw = String(value || '').trim();
|
|
13
|
+
if (!raw) return '';
|
|
14
|
+
return path.resolve(raw.replace(/^~(?=$|[\\/])/, os.homedir()));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function canonicalWorkspacePath(value) {
|
|
18
|
+
const normalized = normalizeWorkspacePath(value);
|
|
19
|
+
if (!normalized) return '';
|
|
20
|
+
try {
|
|
21
|
+
return fs.realpathSync(normalized);
|
|
22
|
+
} catch {
|
|
23
|
+
return normalized;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function rootIdForPath(value) {
|
|
28
|
+
const canonicalPath = canonicalWorkspacePath(value);
|
|
29
|
+
if (!canonicalPath) return '';
|
|
30
|
+
let hash = 0xcbf29ce484222325n;
|
|
31
|
+
for (const byte of Buffer.from(canonicalPath, 'utf8')) {
|
|
32
|
+
hash ^= BigInt(byte);
|
|
33
|
+
hash = BigInt.asUintN(64, hash * 0x100000001b3n);
|
|
34
|
+
}
|
|
35
|
+
return `wroot_${hash.toString(16).padStart(16, '0')}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function projectWorkspaceFromLegacyRef(ref) {
|
|
39
|
+
const value = String(ref || '');
|
|
40
|
+
if (!value.startsWith(PROJECT_FILES_WORKSPACE_PREFIX)) return '';
|
|
41
|
+
try {
|
|
42
|
+
return normalizeWorkspacePath(decodeURIComponent(value.slice(PROJECT_FILES_WORKSPACE_PREFIX.length)));
|
|
43
|
+
} catch {
|
|
44
|
+
return '';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function workspaceRootSnapshot(root) {
|
|
49
|
+
return {
|
|
50
|
+
rootId: root.rootId,
|
|
51
|
+
kind: root.kind,
|
|
52
|
+
canonicalPath: root.canonicalPath,
|
|
53
|
+
repositoryId: root.repositoryId || '',
|
|
54
|
+
accessPolicy: { ...root.accessPolicy },
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
class WorkspaceRootRegistry {
|
|
59
|
+
constructor(agentManager) {
|
|
60
|
+
this.agentManager = agentManager;
|
|
61
|
+
this.roots = new Map();
|
|
62
|
+
this.register({
|
|
63
|
+
rootId: GLOBAL_WORKSPACE_ROOT_ID,
|
|
64
|
+
kind: 'global',
|
|
65
|
+
canonicalPath: GLOBAL_WORKSPACE_FILES_ROOT,
|
|
66
|
+
accessPolicy: { readOnly: true, watch: false, externalReads: false },
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
register(options = {}) {
|
|
71
|
+
const canonicalPath = options.rootId === GLOBAL_WORKSPACE_ROOT_ID
|
|
72
|
+
? GLOBAL_WORKSPACE_FILES_ROOT
|
|
73
|
+
: canonicalWorkspacePath(options.canonicalPath);
|
|
74
|
+
if (!canonicalPath) throw new WorkspaceFileError('workspace root path is required', 400);
|
|
75
|
+
const rootId = options.rootId || rootIdForPath(canonicalPath);
|
|
76
|
+
const current = this.roots.get(rootId);
|
|
77
|
+
if (current && current.canonicalPath !== canonicalPath) {
|
|
78
|
+
throw new WorkspaceFileError('workspace root identity collision', 409);
|
|
79
|
+
}
|
|
80
|
+
const root = Object.freeze({
|
|
81
|
+
rootId,
|
|
82
|
+
kind: options.kind || current?.kind || 'directory',
|
|
83
|
+
canonicalPath,
|
|
84
|
+
repositoryId: options.repositoryId || current?.repositoryId || '',
|
|
85
|
+
accessPolicy: Object.freeze({
|
|
86
|
+
readOnly: options.accessPolicy?.readOnly === true,
|
|
87
|
+
watch: options.accessPolicy?.watch !== false,
|
|
88
|
+
externalReads: options.accessPolicy?.externalReads !== false,
|
|
89
|
+
}),
|
|
90
|
+
});
|
|
91
|
+
this.roots.set(rootId, root);
|
|
92
|
+
return root;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
configuredProjectPaths() {
|
|
96
|
+
const settings = this.agentManager?.configManager?.getSettings?.() || {};
|
|
97
|
+
return (Array.isArray(settings.projectWorkspaces) ? settings.projectWorkspaces : [])
|
|
98
|
+
.map(normalizeWorkspacePath)
|
|
99
|
+
.filter(Boolean);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
liveAgentPaths() {
|
|
103
|
+
const state = this.agentManager?.getState?.() || { agents: [] };
|
|
104
|
+
return (state.agents || []).filter(agent => agent && !agent.isMain).map(agent => ({
|
|
105
|
+
agentId: agent.id,
|
|
106
|
+
path: normalizeWorkspacePath(agent.projectWorkspace || agent.gitWorktree?.workspace || agent.cwd),
|
|
107
|
+
})).filter(entry => entry.path);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
refresh() {
|
|
111
|
+
const activeRootIds = new Set([GLOBAL_WORKSPACE_ROOT_ID]);
|
|
112
|
+
for (const projectPath of this.configuredProjectPaths()) {
|
|
113
|
+
activeRootIds.add(this.register({ kind: 'directory', canonicalPath: projectPath }).rootId);
|
|
114
|
+
}
|
|
115
|
+
for (const entry of this.liveAgentPaths()) {
|
|
116
|
+
activeRootIds.add(this.register({ kind: 'directory', canonicalPath: entry.path }).rootId);
|
|
117
|
+
}
|
|
118
|
+
for (const rootId of this.roots.keys()) {
|
|
119
|
+
if (!activeRootIds.has(rootId)) this.roots.delete(rootId);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
resolve(ref) {
|
|
124
|
+
const value = String(ref || '').trim();
|
|
125
|
+
if (!value) throw new WorkspaceFileError('rootId is required', 400);
|
|
126
|
+
if (value === GLOBAL_WORKSPACE_ROOT_ID || value === GLOBAL_WORKSPACE_FILES_AGENT_ID) {
|
|
127
|
+
return this.roots.get(GLOBAL_WORKSPACE_ROOT_ID);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this.refresh();
|
|
131
|
+
const registered = this.roots.get(value);
|
|
132
|
+
if (registered) return registered;
|
|
133
|
+
|
|
134
|
+
const projectPath = projectWorkspaceFromLegacyRef(value);
|
|
135
|
+
if (projectPath) {
|
|
136
|
+
const authorized = this.configuredProjectPaths().includes(projectPath)
|
|
137
|
+
|| this.liveAgentPaths().some(entry => entry.path === projectPath);
|
|
138
|
+
if (!authorized) throw new WorkspaceFileError('project not found', 404);
|
|
139
|
+
return this.register({ kind: 'directory', canonicalPath: projectPath });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const agentPath = this.liveAgentPaths().find(entry => entry.agentId === value)?.path
|
|
143
|
+
|| normalizeWorkspacePath(this.agentManager?.getAgentWorkspaceRoot?.(value));
|
|
144
|
+
if (agentPath) return this.register({ kind: 'directory', canonicalPath: agentPath });
|
|
145
|
+
throw new WorkspaceFileError(value.startsWith('wroot_') ? 'workspace root not found' : 'agent not found', 404);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
list() {
|
|
149
|
+
this.refresh();
|
|
150
|
+
return [...this.roots.values()].map(workspaceRootSnapshot);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
module.exports = {
|
|
155
|
+
GLOBAL_WORKSPACE_FILES_AGENT_ID,
|
|
156
|
+
GLOBAL_WORKSPACE_FILES_ROOT,
|
|
157
|
+
GLOBAL_WORKSPACE_ROOT_ID,
|
|
158
|
+
PROJECT_FILES_WORKSPACE_PREFIX,
|
|
159
|
+
WorkspaceRootRegistry,
|
|
160
|
+
canonicalWorkspacePath,
|
|
161
|
+
projectWorkspaceFromLegacyRef,
|
|
162
|
+
rootIdForPath,
|
|
163
|
+
workspaceRootSnapshot,
|
|
164
|
+
};
|