imcodes 2026.4.1422-dev.1406 → 2026.4.1427-dev.1416
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.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"command-handler.d.ts","sourceRoot":"","sources":["../../../src/daemon/command-handler.ts"],"names":[],"mappings":"AAQA,OAAO,EAAqC,KAAK,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAEpG,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"command-handler.d.ts","sourceRoot":"","sources":["../../../src/daemon/command-handler.ts"],"names":[],"mappings":"AAQA,OAAO,EAAqC,KAAK,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAEpG,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAqBnD,OAAO,EAAsE,KAAK,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAoO3H,wBAAsB,yBAAyB,CAAC,UAAU,CAAC,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBtF;AAwFD,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,wEAAwE;IACxE,SAAS,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,eAAe,CAAC,EAAE,OAAO,CAAC;QAAC,MAAM,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACzE,uEAAuE;IACvE,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAcD,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,CAoCxD;AAgHD,wGAAwG;AACxG,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAEzD;AAED,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,EAAE,UAAU,EAAE,UAAU,GAAG,IAAI,CAsP3E;AA+7BD,wBAAgB,6BAA6B,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAE7F;AAED,yFAAyF;AACzF,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,OAAO,CAStF;AAED,wBAAgB,8BAA8B,CAAC,MAAM,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,GAAG,MAAM,CAStF;AA4uBD,wBAAgB,qCAAqC,CAAC,IAAI,2CAAgB,4CAEzE;AA0tBD,wBAAgB,0BAA0B,IAAI,IAAI,CAcjD;AA2QD,KAAK,QAAQ,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;CAAE,CAAC;AAEzD,iFAAiF;AACjF,wBAAgB,0BAA0B,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,GAAG,MAAM,CAa3E;AAED,gEAAgE;AAChE,wBAAgB,2BAA2B,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,GAAG,MAAM,CAI5E;AAED,wCAAwC;AACxC,wBAAgB,qBAAqB,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,EAAE,QAAQ,GAAG,MAAM,CAEtE;AAED,uDAAuD;AACvD,wBAAsB,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC,CAEhL"}
|
|
@@ -16,10 +16,12 @@ import logger from '../util/logger.js';
|
|
|
16
16
|
import { homedir } from 'os';
|
|
17
17
|
import { readdir as fsReaddir, realpath as fsRealpath, readFile as fsReadFileRaw, stat as fsStat, writeFile as fsWriteFile } from 'node:fs/promises';
|
|
18
18
|
import * as nodePath from 'node:path';
|
|
19
|
-
import { exec as execCb } from 'node:child_process';
|
|
19
|
+
import { exec as execCb, execFile as execFileCb } from 'node:child_process';
|
|
20
20
|
import { promisify } from 'node:util';
|
|
21
21
|
const execAsync = promisify(execCb);
|
|
22
|
+
const execFileAsync = promisify(execFileCb);
|
|
22
23
|
import { startP2pRun, cancelP2pRun, getP2pRun, listP2pRuns, serializeP2pRun } from './p2p-orchestrator.js';
|
|
24
|
+
import { buildSessionList } from './session-list.js';
|
|
23
25
|
import { getComboRoundCount, parseModePipeline, P2P_CONFIG_MODE } from '../../shared/p2p-modes.js';
|
|
24
26
|
import { CRON_MSG } from '../../shared/cron-types.js';
|
|
25
27
|
import { executeCronJob } from './cron-executor.js';
|
|
@@ -184,7 +186,6 @@ import { resolveContextWindow } from '../util/model-context.js';
|
|
|
184
186
|
import { QWEN_MODEL_IDS } from '../../shared/qwen-models.js';
|
|
185
187
|
import { getQwenRuntimeConfig } from '../agent/qwen-runtime-config.js';
|
|
186
188
|
import { getQwenDisplayMetadata } from '../agent/provider-display.js';
|
|
187
|
-
import { buildSessionList } from './session-list.js';
|
|
188
189
|
import { getQwenOAuthQuotaUsageLabel, recordQwenOAuthRequest } from '../agent/provider-quota.js';
|
|
189
190
|
import { listProviderSessions as listProviderSessionsImpl } from './provider-sessions.js';
|
|
190
191
|
function describeTransportSendError(err) {
|
|
@@ -574,12 +575,66 @@ export function handleWebCommand(msg, serverLink) {
|
|
|
574
575
|
break;
|
|
575
576
|
case 'subsession.rename': {
|
|
576
577
|
const sName = cmd.sessionName;
|
|
577
|
-
const label = cmd.label
|
|
578
|
+
const label = cmd.label === null
|
|
579
|
+
? null
|
|
580
|
+
: (typeof cmd.label === 'string' ? cmd.label : undefined);
|
|
578
581
|
if (sName && label !== undefined) {
|
|
579
582
|
const record = getSession(sName);
|
|
580
583
|
if (record) {
|
|
581
|
-
|
|
584
|
+
const nextLabel = label ?? undefined;
|
|
585
|
+
upsertSession({ ...record, label: nextLabel, updatedAt: Date.now() });
|
|
582
586
|
logger.info({ sessionName: sName, label }, 'subsession.rename: label updated');
|
|
587
|
+
const id = sName.replace(/^deck_sub_/, '');
|
|
588
|
+
void buildSubSessionSync(id, { label: nextLabel }).then((payload) => {
|
|
589
|
+
try {
|
|
590
|
+
serverLink.send(payload);
|
|
591
|
+
}
|
|
592
|
+
catch {
|
|
593
|
+
// not connected
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
break;
|
|
599
|
+
}
|
|
600
|
+
case 'session.rename': {
|
|
601
|
+
const sessionName = cmd.sessionName;
|
|
602
|
+
const projectName = typeof cmd.projectName === 'string' ? cmd.projectName.trim() : '';
|
|
603
|
+
if (sessionName && projectName) {
|
|
604
|
+
const record = getSession(sessionName);
|
|
605
|
+
if (record) {
|
|
606
|
+
upsertSession({ ...record, projectName, updatedAt: Date.now() });
|
|
607
|
+
logger.info({ sessionName, projectName }, 'session.rename: project name updated');
|
|
608
|
+
void buildSessionList().then((sessions) => {
|
|
609
|
+
try {
|
|
610
|
+
serverLink.send({ type: 'session_list', daemonVersion: serverLink.daemonVersion, sessions });
|
|
611
|
+
}
|
|
612
|
+
catch {
|
|
613
|
+
// not connected
|
|
614
|
+
}
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
break;
|
|
619
|
+
}
|
|
620
|
+
case 'session.relabel': {
|
|
621
|
+
const sessionName = cmd.sessionName;
|
|
622
|
+
const label = cmd.label === null
|
|
623
|
+
? null
|
|
624
|
+
: (typeof cmd.label === 'string' ? cmd.label : undefined);
|
|
625
|
+
if (sessionName && label !== undefined) {
|
|
626
|
+
const record = getSession(sessionName);
|
|
627
|
+
if (record) {
|
|
628
|
+
upsertSession({ ...record, label: label ?? undefined, updatedAt: Date.now() });
|
|
629
|
+
logger.info({ sessionName, label }, 'session.relabel: label updated');
|
|
630
|
+
void buildSessionList().then((sessions) => {
|
|
631
|
+
try {
|
|
632
|
+
serverLink.send({ type: 'session_list', daemonVersion: serverLink.daemonVersion, sessions });
|
|
633
|
+
}
|
|
634
|
+
catch {
|
|
635
|
+
// not connected
|
|
636
|
+
}
|
|
637
|
+
});
|
|
583
638
|
}
|
|
584
639
|
}
|
|
585
640
|
break;
|
|
@@ -2664,7 +2719,9 @@ async function handleFsListInner(resolved, rawPath, requestId, includeFiles, inc
|
|
|
2664
2719
|
const FS_READ_SIZE_LIMIT = 512 * 1024; // 512 KB
|
|
2665
2720
|
const fsReadCache = new Map();
|
|
2666
2721
|
const fsReadInflight = new Map();
|
|
2722
|
+
const fsReadGenerations = new Map();
|
|
2667
2723
|
const FS_READ_CACHE_TTL_MS = 5_000;
|
|
2724
|
+
const REPO_CONTEXT_CACHE_TTL_MS = 5_000;
|
|
2668
2725
|
async function loadFsReadSnapshot(realPath, fileSignature) {
|
|
2669
2726
|
const ext = nodePath.extname(realPath).toLowerCase().slice(1);
|
|
2670
2727
|
const IMAGE_MIME = { png: 'image/png', jpg: 'image/jpeg', jpeg: 'image/jpeg', gif: 'image/gif', webp: 'image/webp', ico: 'image/x-icon', bmp: 'image/bmp', svg: 'image/svg+xml' };
|
|
@@ -2677,6 +2734,7 @@ async function loadFsReadSnapshot(realPath, fileSignature) {
|
|
|
2677
2734
|
if (mimeType) {
|
|
2678
2735
|
const buf = await fsReadFileRaw(realPath);
|
|
2679
2736
|
return {
|
|
2737
|
+
path: realPath,
|
|
2680
2738
|
fileSignature,
|
|
2681
2739
|
status: 'ok',
|
|
2682
2740
|
content: buf.toString('base64'),
|
|
@@ -2688,6 +2746,7 @@ async function loadFsReadSnapshot(realPath, fileSignature) {
|
|
|
2688
2746
|
const sample = content.slice(0, 8192);
|
|
2689
2747
|
if (sample.includes('\0')) {
|
|
2690
2748
|
return {
|
|
2749
|
+
path: realPath,
|
|
2691
2750
|
fileSignature,
|
|
2692
2751
|
status: 'error',
|
|
2693
2752
|
error: 'binary_file',
|
|
@@ -2695,6 +2754,7 @@ async function loadFsReadSnapshot(realPath, fileSignature) {
|
|
|
2695
2754
|
};
|
|
2696
2755
|
}
|
|
2697
2756
|
return {
|
|
2757
|
+
path: realPath,
|
|
2698
2758
|
fileSignature,
|
|
2699
2759
|
status: 'ok',
|
|
2700
2760
|
content,
|
|
@@ -2705,18 +2765,23 @@ async function getFsReadSnapshot(realPath, fileSignature) {
|
|
|
2705
2765
|
if (cached && cached.expiresAt > Date.now() && cached.value.fileSignature === fileSignature) {
|
|
2706
2766
|
return cached.value;
|
|
2707
2767
|
}
|
|
2708
|
-
const
|
|
2768
|
+
const generation = getResourceGeneration(fsReadGenerations, realPath);
|
|
2769
|
+
const inflightKey = `${realPath}::${fileSignature}::${generation}`;
|
|
2770
|
+
const inflight = fsReadInflight.get(inflightKey);
|
|
2709
2771
|
if (inflight)
|
|
2710
2772
|
return await inflight;
|
|
2711
2773
|
const promise = loadFsReadSnapshot(realPath, fileSignature)
|
|
2712
|
-
.then((value) => {
|
|
2713
|
-
|
|
2774
|
+
.then(async (value) => {
|
|
2775
|
+
const currentSignature = await safeStatSignature(realPath);
|
|
2776
|
+
if (getResourceGeneration(fsReadGenerations, realPath) === generation && currentSignature === value.fileSignature) {
|
|
2777
|
+
fsReadCache.set(realPath, { value, expiresAt: Date.now() + FS_READ_CACHE_TTL_MS });
|
|
2778
|
+
}
|
|
2714
2779
|
return value;
|
|
2715
2780
|
})
|
|
2716
2781
|
.finally(() => {
|
|
2717
|
-
fsReadInflight.delete(
|
|
2782
|
+
fsReadInflight.delete(inflightKey);
|
|
2718
2783
|
});
|
|
2719
|
-
fsReadInflight.set(
|
|
2784
|
+
fsReadInflight.set(inflightKey, promise);
|
|
2720
2785
|
return await promise;
|
|
2721
2786
|
}
|
|
2722
2787
|
async function handleFsRead(cmd, serverLink) {
|
|
@@ -2793,15 +2858,25 @@ async function handleFsRead(cmd, serverLink) {
|
|
|
2793
2858
|
}
|
|
2794
2859
|
const GIT_STATUS_CACHE_TTL_MS = 5_000;
|
|
2795
2860
|
const GIT_DIFF_CACHE_TTL_MS = 5_000;
|
|
2861
|
+
const repoContextCache = new Map();
|
|
2862
|
+
const repoSignatureCache = new Map();
|
|
2796
2863
|
const gitStatusCache = new Map();
|
|
2797
2864
|
const gitStatusInflight = new Map();
|
|
2798
2865
|
const gitNumstatCache = new Map();
|
|
2799
2866
|
const gitNumstatInflight = new Map();
|
|
2800
2867
|
const gitDiffCache = new Map();
|
|
2801
2868
|
const gitDiffInflight = new Map();
|
|
2869
|
+
const gitRepoGenerations = new Map();
|
|
2870
|
+
const gitDiffGenerations = new Map();
|
|
2802
2871
|
function normalizeFsPath(value) {
|
|
2803
2872
|
return nodePath.resolve(value);
|
|
2804
2873
|
}
|
|
2874
|
+
function getResourceGeneration(map, key) {
|
|
2875
|
+
return map.get(key) ?? 0;
|
|
2876
|
+
}
|
|
2877
|
+
function bumpResourceGeneration(map, key) {
|
|
2878
|
+
map.set(key, getResourceGeneration(map, key) + 1);
|
|
2879
|
+
}
|
|
2805
2880
|
function isPathInside(root, candidate) {
|
|
2806
2881
|
const normalizedRoot = normalizeFsPath(root);
|
|
2807
2882
|
const normalizedCandidate = normalizeFsPath(candidate);
|
|
@@ -2833,56 +2908,126 @@ async function resolveGitDir(dotGitPath, repoRoot) {
|
|
|
2833
2908
|
return null;
|
|
2834
2909
|
}
|
|
2835
2910
|
}
|
|
2836
|
-
async function
|
|
2911
|
+
async function findRepoContextBase(startPath) {
|
|
2837
2912
|
let current = normalizeFsPath(startPath);
|
|
2913
|
+
const traversed = [];
|
|
2914
|
+
const now = Date.now();
|
|
2838
2915
|
while (true) {
|
|
2916
|
+
const cached = repoContextCache.get(current);
|
|
2917
|
+
if (cached && cached.expiresAt > now) {
|
|
2918
|
+
for (const traversedPath of traversed) {
|
|
2919
|
+
repoContextCache.set(traversedPath, { expiresAt: now + REPO_CONTEXT_CACHE_TTL_MS, value: cached.value });
|
|
2920
|
+
}
|
|
2921
|
+
return cached.value;
|
|
2922
|
+
}
|
|
2923
|
+
traversed.push(current);
|
|
2839
2924
|
const dotGit = nodePath.join(current, '.git');
|
|
2840
2925
|
const gitDir = await resolveGitDir(dotGit, current);
|
|
2841
|
-
if (gitDir)
|
|
2842
|
-
|
|
2926
|
+
if (gitDir) {
|
|
2927
|
+
const value = { repoRoot: current, gitDir };
|
|
2928
|
+
for (const traversedPath of traversed) {
|
|
2929
|
+
repoContextCache.set(traversedPath, { expiresAt: Date.now() + REPO_CONTEXT_CACHE_TTL_MS, value });
|
|
2930
|
+
}
|
|
2931
|
+
return value;
|
|
2932
|
+
}
|
|
2843
2933
|
const parent = nodePath.dirname(current);
|
|
2844
2934
|
if (parent === current)
|
|
2845
2935
|
break;
|
|
2846
2936
|
current = parent;
|
|
2847
2937
|
}
|
|
2938
|
+
for (const traversedPath of traversed) {
|
|
2939
|
+
repoContextCache.set(traversedPath, { expiresAt: Date.now() + REPO_CONTEXT_CACHE_TTL_MS, value: null });
|
|
2940
|
+
}
|
|
2848
2941
|
return null;
|
|
2849
2942
|
}
|
|
2850
|
-
async function
|
|
2851
|
-
const
|
|
2943
|
+
async function buildRepoSignatureState(gitDir, indexSig, headSig) {
|
|
2944
|
+
const resolvedIndexSig = indexSig ?? await safeStatSignature(nodePath.join(gitDir, 'index'));
|
|
2852
2945
|
const headPath = nodePath.join(gitDir, 'HEAD');
|
|
2853
|
-
const
|
|
2946
|
+
const resolvedHeadSig = headSig ?? await safeStatSignature(headPath);
|
|
2947
|
+
let refPath = null;
|
|
2854
2948
|
let refSig = 'none';
|
|
2855
2949
|
try {
|
|
2856
2950
|
const headRaw = await fsReadFileRaw(headPath, 'utf8');
|
|
2857
2951
|
const match = headRaw.match(/^ref:\s*(.+)\s*$/m);
|
|
2858
2952
|
if (match?.[1]) {
|
|
2859
|
-
|
|
2953
|
+
refPath = match[1].trim();
|
|
2954
|
+
refSig = await safeStatSignature(nodePath.join(gitDir, refPath));
|
|
2860
2955
|
}
|
|
2861
2956
|
}
|
|
2862
2957
|
catch {
|
|
2863
2958
|
refSig = 'missing';
|
|
2864
2959
|
}
|
|
2865
|
-
return
|
|
2960
|
+
return {
|
|
2961
|
+
repoSignature: `${resolvedIndexSig}|${resolvedHeadSig}|${refSig}`,
|
|
2962
|
+
indexSig: resolvedIndexSig,
|
|
2963
|
+
headSig: resolvedHeadSig,
|
|
2964
|
+
refPath,
|
|
2965
|
+
refSig,
|
|
2966
|
+
};
|
|
2967
|
+
}
|
|
2968
|
+
async function getRepoSignature(repoRoot, gitDir) {
|
|
2969
|
+
const indexSig = await safeStatSignature(nodePath.join(gitDir, 'index'));
|
|
2970
|
+
const headPath = nodePath.join(gitDir, 'HEAD');
|
|
2971
|
+
const headSig = await safeStatSignature(headPath);
|
|
2972
|
+
const cached = repoSignatureCache.get(repoRoot);
|
|
2973
|
+
if (cached && cached.indexSig === indexSig && cached.headSig === headSig) {
|
|
2974
|
+
if (!cached.refPath || await safeStatSignature(nodePath.join(gitDir, cached.refPath)) === cached.refSig) {
|
|
2975
|
+
return cached.repoSignature;
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
const next = await buildRepoSignatureState(gitDir, indexSig, headSig);
|
|
2979
|
+
repoSignatureCache.set(repoRoot, next);
|
|
2980
|
+
return next.repoSignature;
|
|
2866
2981
|
}
|
|
2867
2982
|
async function resolveRepoContext(startPath) {
|
|
2868
|
-
const repo = await
|
|
2983
|
+
const repo = await findRepoContextBase(startPath);
|
|
2869
2984
|
if (!repo)
|
|
2870
2985
|
return null;
|
|
2871
2986
|
return {
|
|
2872
2987
|
repoRoot: repo.repoRoot,
|
|
2873
2988
|
gitDir: repo.gitDir,
|
|
2874
|
-
repoSignature: await
|
|
2989
|
+
repoSignature: await getRepoSignature(repo.repoRoot, repo.gitDir),
|
|
2875
2990
|
};
|
|
2876
2991
|
}
|
|
2992
|
+
function decodeGitPath(rawPath) {
|
|
2993
|
+
return rawPath.replace(/\\([\\\"abfnrtv])/g, (_match, escaped) => {
|
|
2994
|
+
switch (escaped) {
|
|
2995
|
+
case 'a': return '\u0007';
|
|
2996
|
+
case 'b': return '\b';
|
|
2997
|
+
case 'f': return '\f';
|
|
2998
|
+
case 'n': return '\n';
|
|
2999
|
+
case 'r': return '\r';
|
|
3000
|
+
case 't': return '\t';
|
|
3001
|
+
case 'v': return '\v';
|
|
3002
|
+
case '\\': return '\\';
|
|
3003
|
+
case '"': return '"';
|
|
3004
|
+
default: return escaped;
|
|
3005
|
+
}
|
|
3006
|
+
}).replace(/\\([0-7]{1,3})/g, (_match, octal) => String.fromCharCode(parseInt(octal, 8)));
|
|
3007
|
+
}
|
|
3008
|
+
function parseZRecords(stdout) {
|
|
3009
|
+
return stdout.split('\0').filter((entry) => entry.length > 0);
|
|
3010
|
+
}
|
|
3011
|
+
function normalizeRepoRelativePath(repoRoot, relativePath) {
|
|
3012
|
+
return nodePath.join(repoRoot, decodeGitPath(relativePath));
|
|
3013
|
+
}
|
|
2877
3014
|
async function loadRepoGitStatusSnapshot(repoRoot, repoSignature) {
|
|
2878
|
-
const { stdout } = await execAsync('git status --porcelain -u', { cwd: repoRoot, timeout: 5000 });
|
|
3015
|
+
const { stdout } = await execAsync('git status --porcelain=v1 -z -u', { cwd: repoRoot, timeout: 5000, encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 });
|
|
2879
3016
|
const files = [];
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
const code =
|
|
2884
|
-
const
|
|
2885
|
-
|
|
3017
|
+
const records = parseZRecords(stdout);
|
|
3018
|
+
for (let idx = 0; idx < records.length; idx++) {
|
|
3019
|
+
const record = records[idx];
|
|
3020
|
+
const code = record.slice(0, 2).trim();
|
|
3021
|
+
const firstPath = record.slice(3);
|
|
3022
|
+
let logicalPath = firstPath;
|
|
3023
|
+
if (code.startsWith('R') || code.startsWith('C')) {
|
|
3024
|
+
const renamedTo = records[idx + 1];
|
|
3025
|
+
if (renamedTo) {
|
|
3026
|
+
logicalPath = renamedTo;
|
|
3027
|
+
idx += 1;
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
files.push({ path: normalizeRepoRelativePath(repoRoot, logicalPath), code });
|
|
2886
3031
|
}
|
|
2887
3032
|
return { repoRoot, repoSignature, files };
|
|
2888
3033
|
}
|
|
@@ -2894,41 +3039,60 @@ async function getRepoGitStatusSnapshot(startPath) {
|
|
|
2894
3039
|
if (cached && cached.expiresAt > Date.now() && cached.value.repoSignature === context.repoSignature) {
|
|
2895
3040
|
return cached.value;
|
|
2896
3041
|
}
|
|
2897
|
-
const
|
|
3042
|
+
const generation = getResourceGeneration(gitRepoGenerations, context.repoRoot);
|
|
3043
|
+
const inflightKey = `${context.repoRoot}::${context.repoSignature}::${generation}`;
|
|
3044
|
+
const inflight = gitStatusInflight.get(inflightKey);
|
|
2898
3045
|
if (inflight)
|
|
2899
3046
|
return await inflight;
|
|
2900
3047
|
const promise = loadRepoGitStatusSnapshot(context.repoRoot, context.repoSignature)
|
|
2901
|
-
.then((value) => {
|
|
2902
|
-
|
|
3048
|
+
.then(async (value) => {
|
|
3049
|
+
const currentSignature = await getRepoSignature(context.repoRoot, context.gitDir);
|
|
3050
|
+
if (getResourceGeneration(gitRepoGenerations, context.repoRoot) === generation && currentSignature === value.repoSignature) {
|
|
3051
|
+
gitStatusCache.set(context.repoRoot, { value, expiresAt: Date.now() + GIT_STATUS_CACHE_TTL_MS });
|
|
3052
|
+
}
|
|
2903
3053
|
return value;
|
|
2904
3054
|
})
|
|
2905
3055
|
.finally(() => {
|
|
2906
|
-
gitStatusInflight.delete(
|
|
3056
|
+
gitStatusInflight.delete(inflightKey);
|
|
2907
3057
|
});
|
|
2908
|
-
gitStatusInflight.set(
|
|
3058
|
+
gitStatusInflight.set(inflightKey, promise);
|
|
2909
3059
|
return await promise;
|
|
2910
3060
|
}
|
|
2911
3061
|
async function loadRepoGitNumstatSnapshot(repoRoot, repoSignature) {
|
|
2912
3062
|
let stdout = '';
|
|
2913
3063
|
try {
|
|
2914
|
-
({ stdout } = await execAsync('git diff --numstat HEAD', { cwd: repoRoot, timeout: 5000 }));
|
|
3064
|
+
({ stdout } = await execAsync('git diff --numstat -z HEAD', { cwd: repoRoot, timeout: 5000, encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 }));
|
|
2915
3065
|
}
|
|
2916
3066
|
catch {
|
|
2917
3067
|
try {
|
|
2918
|
-
({ stdout } = await execAsync('git diff --numstat', { cwd: repoRoot, timeout: 5000 }));
|
|
3068
|
+
({ stdout } = await execAsync('git diff --numstat -z', { cwd: repoRoot, timeout: 5000, encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 }));
|
|
2919
3069
|
}
|
|
2920
3070
|
catch {
|
|
2921
3071
|
stdout = '';
|
|
2922
3072
|
}
|
|
2923
3073
|
}
|
|
2924
3074
|
const stats = new Map();
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
3075
|
+
const records = parseZRecords(stdout);
|
|
3076
|
+
for (let idx = 0; idx < records.length; idx++) {
|
|
3077
|
+
const header = records[idx];
|
|
3078
|
+
const firstTab = header.indexOf('\t');
|
|
3079
|
+
const secondTab = firstTab >= 0 ? header.indexOf('\t', firstTab + 1) : -1;
|
|
3080
|
+
if (firstTab < 0 || secondTab < 0)
|
|
2928
3081
|
continue;
|
|
2929
|
-
const
|
|
2930
|
-
const
|
|
2931
|
-
|
|
3082
|
+
const additionsRaw = header.slice(0, firstTab);
|
|
3083
|
+
const deletionsRaw = header.slice(firstTab + 1, secondTab);
|
|
3084
|
+
const pathRaw = header.slice(secondTab + 1);
|
|
3085
|
+
const additions = additionsRaw === '-' ? undefined : parseInt(additionsRaw, 10);
|
|
3086
|
+
const deletions = deletionsRaw === '-' ? undefined : parseInt(deletionsRaw, 10);
|
|
3087
|
+
let logicalPath = pathRaw;
|
|
3088
|
+
if (pathRaw === '') {
|
|
3089
|
+
const renamedTo = records[idx + 2];
|
|
3090
|
+
if (!renamedTo)
|
|
3091
|
+
continue;
|
|
3092
|
+
logicalPath = renamedTo;
|
|
3093
|
+
idx += 2;
|
|
3094
|
+
}
|
|
3095
|
+
stats.set(normalizeRepoRelativePath(repoRoot, logicalPath), { additions, deletions });
|
|
2932
3096
|
}
|
|
2933
3097
|
return { repoRoot, repoSignature, stats };
|
|
2934
3098
|
}
|
|
@@ -2940,89 +3104,163 @@ async function getRepoGitNumstatSnapshot(startPath) {
|
|
|
2940
3104
|
if (cached && cached.expiresAt > Date.now() && cached.value.repoSignature === context.repoSignature) {
|
|
2941
3105
|
return cached.value;
|
|
2942
3106
|
}
|
|
2943
|
-
const
|
|
3107
|
+
const generation = getResourceGeneration(gitRepoGenerations, context.repoRoot);
|
|
3108
|
+
const inflightKey = `${context.repoRoot}::${context.repoSignature}::${generation}`;
|
|
3109
|
+
const inflight = gitNumstatInflight.get(inflightKey);
|
|
2944
3110
|
if (inflight)
|
|
2945
3111
|
return await inflight;
|
|
2946
3112
|
const promise = loadRepoGitNumstatSnapshot(context.repoRoot, context.repoSignature)
|
|
2947
|
-
.then((value) => {
|
|
2948
|
-
|
|
3113
|
+
.then(async (value) => {
|
|
3114
|
+
const currentSignature = await getRepoSignature(context.repoRoot, context.gitDir);
|
|
3115
|
+
if (getResourceGeneration(gitRepoGenerations, context.repoRoot) === generation && currentSignature === value.repoSignature) {
|
|
3116
|
+
gitNumstatCache.set(context.repoRoot, { value, expiresAt: Date.now() + GIT_STATUS_CACHE_TTL_MS });
|
|
3117
|
+
}
|
|
2949
3118
|
return value;
|
|
2950
3119
|
})
|
|
2951
3120
|
.finally(() => {
|
|
2952
|
-
gitNumstatInflight.delete(
|
|
3121
|
+
gitNumstatInflight.delete(inflightKey);
|
|
2953
3122
|
});
|
|
2954
|
-
gitNumstatInflight.set(
|
|
3123
|
+
gitNumstatInflight.set(inflightKey, promise);
|
|
2955
3124
|
return await promise;
|
|
2956
3125
|
}
|
|
2957
|
-
async function loadFileGitDiffSnapshot(
|
|
3126
|
+
async function loadFileGitDiffSnapshot(logicalPath, repoRoot, repoSignature, fileSignature) {
|
|
2958
3127
|
let diff = '';
|
|
3128
|
+
const repoRelativePath = nodePath.relative(repoRoot, logicalPath).split(nodePath.sep).join('/');
|
|
2959
3129
|
try {
|
|
2960
|
-
const { stdout } = await
|
|
3130
|
+
const { stdout } = await execFileAsync('git', ['diff', 'HEAD', '--', repoRelativePath], { cwd: repoRoot, timeout: 5000, encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 });
|
|
2961
3131
|
diff = stdout;
|
|
2962
3132
|
}
|
|
2963
3133
|
catch { /* ignore */ }
|
|
2964
3134
|
if (!diff) {
|
|
2965
3135
|
try {
|
|
2966
|
-
const { stdout } = await
|
|
3136
|
+
const { stdout } = await execFileAsync('git', ['diff', '--', repoRelativePath], { cwd: repoRoot, timeout: 5000, encoding: 'utf8', maxBuffer: 10 * 1024 * 1024 });
|
|
2967
3137
|
diff = stdout;
|
|
2968
3138
|
}
|
|
2969
3139
|
catch { /* ignore */ }
|
|
2970
3140
|
}
|
|
2971
|
-
return { repoRoot, repoSignature, fileSignature, diff };
|
|
3141
|
+
return { logicalPath, repoRoot, repoSignature, fileSignature, diff };
|
|
2972
3142
|
}
|
|
2973
|
-
async function getFileGitDiffSnapshot(
|
|
2974
|
-
const context = await resolveRepoContext(nodePath.dirname(
|
|
3143
|
+
async function getFileGitDiffSnapshot(logicalPath) {
|
|
3144
|
+
const context = await resolveRepoContext(nodePath.dirname(logicalPath));
|
|
2975
3145
|
if (!context)
|
|
2976
3146
|
return null;
|
|
2977
|
-
const fileSignature = await safeStatSignature(
|
|
2978
|
-
const cached = gitDiffCache.get(
|
|
3147
|
+
const fileSignature = await safeStatSignature(logicalPath);
|
|
3148
|
+
const cached = gitDiffCache.get(logicalPath);
|
|
2979
3149
|
if (cached
|
|
2980
3150
|
&& cached.expiresAt > Date.now()
|
|
2981
3151
|
&& cached.value.repoSignature === context.repoSignature
|
|
2982
3152
|
&& cached.value.fileSignature === fileSignature) {
|
|
2983
3153
|
return cached.value;
|
|
2984
3154
|
}
|
|
2985
|
-
const
|
|
3155
|
+
const generation = getResourceGeneration(gitDiffGenerations, logicalPath);
|
|
3156
|
+
const inflightKey = `${logicalPath}::${context.repoSignature}::${fileSignature}::${generation}`;
|
|
3157
|
+
const inflight = gitDiffInflight.get(inflightKey);
|
|
2986
3158
|
if (inflight)
|
|
2987
3159
|
return await inflight;
|
|
2988
|
-
const promise = loadFileGitDiffSnapshot(
|
|
2989
|
-
.then((value) => {
|
|
2990
|
-
|
|
3160
|
+
const promise = loadFileGitDiffSnapshot(logicalPath, context.repoRoot, context.repoSignature, fileSignature)
|
|
3161
|
+
.then(async (value) => {
|
|
3162
|
+
const currentContext = await resolveRepoContext(nodePath.dirname(logicalPath));
|
|
3163
|
+
const currentFileSignature = await safeStatSignature(logicalPath);
|
|
3164
|
+
if (getResourceGeneration(gitDiffGenerations, logicalPath) === generation
|
|
3165
|
+
&& currentContext
|
|
3166
|
+
&& currentContext.repoSignature === value.repoSignature
|
|
3167
|
+
&& currentFileSignature === value.fileSignature) {
|
|
3168
|
+
gitDiffCache.set(logicalPath, { value, expiresAt: Date.now() + GIT_DIFF_CACHE_TTL_MS });
|
|
3169
|
+
}
|
|
2991
3170
|
return value;
|
|
2992
3171
|
})
|
|
2993
3172
|
.finally(() => {
|
|
2994
|
-
gitDiffInflight.delete(
|
|
3173
|
+
gitDiffInflight.delete(inflightKey);
|
|
2995
3174
|
});
|
|
2996
|
-
gitDiffInflight.set(
|
|
3175
|
+
gitDiffInflight.set(inflightKey, promise);
|
|
2997
3176
|
return await promise;
|
|
2998
3177
|
}
|
|
2999
|
-
function
|
|
3000
|
-
|
|
3178
|
+
function collectAffectedRepoRoots(targetPath) {
|
|
3179
|
+
const affected = new Set();
|
|
3180
|
+
for (const key of gitStatusCache.keys()) {
|
|
3181
|
+
if (isPathInside(key, targetPath))
|
|
3182
|
+
affected.add(key);
|
|
3183
|
+
}
|
|
3184
|
+
for (const key of gitNumstatCache.keys()) {
|
|
3185
|
+
if (isPathInside(key, targetPath))
|
|
3186
|
+
affected.add(key);
|
|
3187
|
+
}
|
|
3188
|
+
for (const key of gitStatusInflight.keys()) {
|
|
3189
|
+
const repoRoot = key.split('::')[0] ?? '';
|
|
3190
|
+
if (repoRoot && isPathInside(repoRoot, targetPath))
|
|
3191
|
+
affected.add(repoRoot);
|
|
3192
|
+
}
|
|
3193
|
+
for (const key of gitNumstatInflight.keys()) {
|
|
3194
|
+
const repoRoot = key.split('::')[0] ?? '';
|
|
3195
|
+
if (repoRoot && isPathInside(repoRoot, targetPath))
|
|
3196
|
+
affected.add(repoRoot);
|
|
3197
|
+
}
|
|
3198
|
+
for (const entry of repoContextCache.values()) {
|
|
3199
|
+
const repoRoot = entry.value?.repoRoot;
|
|
3200
|
+
if (repoRoot && isPathInside(repoRoot, targetPath))
|
|
3201
|
+
affected.add(repoRoot);
|
|
3202
|
+
}
|
|
3203
|
+
return affected;
|
|
3001
3204
|
}
|
|
3002
3205
|
function invalidateGitCachesForPath(targetPath) {
|
|
3003
3206
|
const normalized = normalizeFsPath(targetPath);
|
|
3207
|
+
bumpResourceGeneration(fsReadGenerations, normalized);
|
|
3208
|
+
bumpResourceGeneration(gitDiffGenerations, normalized);
|
|
3209
|
+
for (const repoRoot of collectAffectedRepoRoots(normalized)) {
|
|
3210
|
+
bumpResourceGeneration(gitRepoGenerations, repoRoot);
|
|
3211
|
+
}
|
|
3004
3212
|
fsReadCache.delete(normalized);
|
|
3005
|
-
fsReadInflight.delete(normalized);
|
|
3006
3213
|
gitDiffCache.delete(normalized);
|
|
3007
|
-
|
|
3214
|
+
for (const key of fsReadInflight.keys()) {
|
|
3215
|
+
if (key.startsWith(`${normalized}::`))
|
|
3216
|
+
fsReadInflight.delete(key);
|
|
3217
|
+
}
|
|
3218
|
+
for (const key of gitDiffInflight.keys()) {
|
|
3219
|
+
if (key.startsWith(`${normalized}::`))
|
|
3220
|
+
gitDiffInflight.delete(key);
|
|
3221
|
+
}
|
|
3008
3222
|
for (const key of gitStatusCache.keys()) {
|
|
3009
3223
|
if (isPathInside(key, normalized))
|
|
3010
3224
|
gitStatusCache.delete(key);
|
|
3225
|
+
if (isPathInside(key, normalized))
|
|
3226
|
+
repoSignatureCache.delete(key);
|
|
3227
|
+
}
|
|
3228
|
+
for (const key of repoContextCache.keys()) {
|
|
3229
|
+
if (isPathInside(key, normalized))
|
|
3230
|
+
repoContextCache.delete(key);
|
|
3011
3231
|
}
|
|
3012
3232
|
for (const key of gitNumstatCache.keys()) {
|
|
3013
3233
|
if (isPathInside(key, normalized))
|
|
3014
3234
|
gitNumstatCache.delete(key);
|
|
3235
|
+
if (isPathInside(key, normalized))
|
|
3236
|
+
repoSignatureCache.delete(key);
|
|
3237
|
+
}
|
|
3238
|
+
for (const key of gitStatusInflight.keys()) {
|
|
3239
|
+
if (isPathInside(key.split('::')[0] ?? '', normalized))
|
|
3240
|
+
gitStatusInflight.delete(key);
|
|
3241
|
+
}
|
|
3242
|
+
for (const key of gitNumstatInflight.keys()) {
|
|
3243
|
+
if (isPathInside(key.split('::')[0] ?? '', normalized))
|
|
3244
|
+
gitNumstatInflight.delete(key);
|
|
3015
3245
|
}
|
|
3016
3246
|
}
|
|
3017
3247
|
export function __resetFsGitCachesForTests() {
|
|
3018
3248
|
fsReadCache.clear();
|
|
3019
3249
|
fsReadInflight.clear();
|
|
3250
|
+
fsReadGenerations.clear();
|
|
3251
|
+
repoContextCache.clear();
|
|
3252
|
+
repoSignatureCache.clear();
|
|
3020
3253
|
gitStatusCache.clear();
|
|
3021
3254
|
gitStatusInflight.clear();
|
|
3022
3255
|
gitNumstatCache.clear();
|
|
3023
3256
|
gitNumstatInflight.clear();
|
|
3024
3257
|
gitDiffCache.clear();
|
|
3025
3258
|
gitDiffInflight.clear();
|
|
3259
|
+
gitRepoGenerations.clear();
|
|
3260
|
+
gitDiffGenerations.clear();
|
|
3261
|
+
}
|
|
3262
|
+
function filterRepoFilesForPath(files, requestedPath) {
|
|
3263
|
+
return files.filter((file) => isPathInside(requestedPath, file.path));
|
|
3026
3264
|
}
|
|
3027
3265
|
/** fs.git_status — return git modified file list for a directory */
|
|
3028
3266
|
async function handleFsGitStatus(cmd, serverLink) {
|
|
@@ -3075,8 +3313,20 @@ async function handleFsGitDiff(cmd, serverLink) {
|
|
|
3075
3313
|
const expanded = rawPath.startsWith('~') ? rawPath.replace(/^~/, homedir()) : rawPath;
|
|
3076
3314
|
const resolved = nodePath.resolve(expanded);
|
|
3077
3315
|
try {
|
|
3078
|
-
|
|
3079
|
-
|
|
3316
|
+
let allowedProbe = resolved;
|
|
3317
|
+
while (true) {
|
|
3318
|
+
try {
|
|
3319
|
+
allowedProbe = await fsRealpath(allowedProbe);
|
|
3320
|
+
break;
|
|
3321
|
+
}
|
|
3322
|
+
catch {
|
|
3323
|
+
const parent = nodePath.dirname(allowedProbe);
|
|
3324
|
+
if (parent === allowedProbe)
|
|
3325
|
+
throw new Error(`ENOENT: no such file or directory, realpath '${resolved}'`);
|
|
3326
|
+
allowedProbe = parent;
|
|
3327
|
+
}
|
|
3328
|
+
}
|
|
3329
|
+
const allowed = isPathAllowed(allowedProbe);
|
|
3080
3330
|
if (!allowed) {
|
|
3081
3331
|
try {
|
|
3082
3332
|
serverLink.send({ type: 'fs.git_diff_response', requestId, path: rawPath, status: 'error', error: 'forbidden_path' });
|
|
@@ -3084,11 +3334,11 @@ async function handleFsGitDiff(cmd, serverLink) {
|
|
|
3084
3334
|
catch { /* ignore */ }
|
|
3085
3335
|
return;
|
|
3086
3336
|
}
|
|
3087
|
-
const snapshot = await getFileGitDiffSnapshot(
|
|
3337
|
+
const snapshot = await getFileGitDiffSnapshot(resolved);
|
|
3088
3338
|
const diff = snapshot?.diff ?? '';
|
|
3089
3339
|
// Untracked files: no diff (nothing meaningful to compare against)
|
|
3090
3340
|
try {
|
|
3091
|
-
serverLink.send({ type: 'fs.git_diff_response', requestId, path: rawPath, resolvedPath:
|
|
3341
|
+
serverLink.send({ type: 'fs.git_diff_response', requestId, path: rawPath, resolvedPath: resolved, status: 'ok', diff });
|
|
3092
3342
|
}
|
|
3093
3343
|
catch { /* ignore */ }
|
|
3094
3344
|
}
|